diff --git a/agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md b/agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md deleted file mode 100644 index 2165bb9b..00000000 --- a/agent-task/m-client-validation/04+03_fcm_ack_smoke/CODE_REVIEW-cloud-G07.md +++ /dev/null @@ -1,239 +0,0 @@ - - -# 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. -> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. -> Fill implementation-owned sections, then stop with active files in place and report ready for review. -> If implementation is blocked by a user-only decision, external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. -> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. -> Follow the ownership table at the bottom of this file for which sections you own. - -## 개요 - -date=2026-05-29 -task=m-client-validation/04+03_fcm_ack_smoke, plan=1, tag=REVIEW_SMOKE_RUN - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. - -각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. -리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. - -1. 판정을 append한다. -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-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. -5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [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 정리 | [ ] | - -## 구현 체크리스트 - -- [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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - -## 코드리뷰 전용 체크리스트 - -> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. -> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. - -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다. -- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. -- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. -- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다. -- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. -- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. - -## 계획 대비 변경 사항 - -- 사용자 추가 지시("너가 일단 진행해봐")에 따라 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를 수집한다. - -## 리뷰어를 위한 체크포인트 - -- 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 중 재검토 가능한 형태인지 본다. - -## 검증 결과 - -_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ - -필수 규칙: -- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. -- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. -- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. -- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. - -### REVIEW_SMOKE_RUN-1 중간 검증 -``` -$ test -f agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log -exit=0 - -$ 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 충족. - -### REVIEW_SMOKE_RUN-2 중간 검증 -``` -$ 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 -# 위 표/근거로 매칭됨. -``` - -### 최종 검증 -``` -$ 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 파일 유지. - ---- - -> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** -> If anything is blank, go back and fill it in before saving this file. -> Leave review-agent-only sections unchanged. diff --git a/agent-task/m-client-validation/04+03_fcm_ack_smoke/PLAN-cloud-G07.md b/agent-task/m-client-validation/04+03_fcm_ack_smoke/PLAN-cloud-G07.md deleted file mode 100644 index 2c01f198..00000000 --- a/agent-task/m-client-validation/04+03_fcm_ack_smoke/PLAN-cloud-G07.md +++ /dev/null @@ -1,250 +0,0 @@ - - -# Plan - REVIEW_SMOKE_RUN: Resume Real FCM And ACK Smoke - -## 이 파일을 읽는 구현 에이전트에게 - -이 계획의 구현이 끝나도 작업은 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용으로 채우기 전까지 완료가 아니다. 검증 명령을 실행하고 실제 stdout/stderr를 붙이며, active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 사용자만 줄 수 있는 결정, 외부 환경 준비, 또는 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션을 정확한 근거와 재개 조건으로 채우고 멈춘다. `USER_REVIEW.md`, `complete.log`, archive 이동은 code-review 전용이다. - -## 배경 - -이전 review stop은 `03_smoke_preflight`를 active 경로에서만 찾아 생긴 false blocker였다. 선행 preflight는 archive에 PASS 완료되어 있고, 원격 Android host와 trigger env도 확인됐다. 이 계획은 archived preflight evidence를 승계해 실제 FCM delivery와 ACK smoke를 다시 진행한다. - -## 사용자 리뷰 요청 흐름 - -구현 중 차단 사유가 생기면 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션을 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 채운다. code-review가 그 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. - -## 분석 결과 - -### 읽은 파일 - -- `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` -- `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 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. - -### 심볼 참조 - -- none. public symbol rename/remove 없음. - -### 분할 판단 - -Split decision policy was evaluated. This remains one dependent subtask under shared task group `m-client-validation`. - -- 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 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`. This depends on remote shell, `flutter run`, adb/logcat, external FCM/push-proxy behavior, stdout/stderr interpretation, and manual evidence. - -## 구현 체크리스트 - -- [ ] [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`에 의존한다. 선행 완료 경로는 active가 아니라 archive의 `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`다. 구현 시작 전 이 경로를 확인하고, active-only 경로 부재를 blocker로 보지 않는다. - -## [REVIEW_SMOKE_RUN-1] Preflight와 remote readiness 승계 - -### 문제 - -`user_review_0.log`는 `03_smoke_preflight/complete.log` active 경로 부재를 blocker로 기록했다. 실제 완료 산출물은 `agent-task/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log`에 있다. - -### 해결 방법 - -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/archive/2026/05/m-client-validation/03_smoke_preflight/complete.log` 존재 확인. -- [ ] remote `adb`, `flutter`, connected Android device 확인. -- [ ] `NEXO_SMOKE_TRIGGER_CMD` 존재 확인. -- [ ] smoke runtime 기본값을 secret 없이 기록. - -### 테스트 작성 - -테스트 파일 추가 없음. 이 항목은 remote smoke readiness 검증이다. - -### 중간 검증 - -```sh -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}" -'"'"' -``` - -기대 결과: 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. - -## [REVIEW_SMOKE_RUN-2] Android app 실행과 FCM token evidence - -### 문제 - -FCM send requires a raw Firebase token. The test app displays the plugin-formatted token as `android_rn-v2:` in `apps/flutter-test/lib/main.dart`. - -### 해결 방법 - -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`로 사용한다. - -### 수정 파일 및 체크리스트 - -- [ ] remote app dependency 준비. -- [ ] logcat buffer clear. -- [ ] app 실행 output 저장. -- [ ] `android_rn-v2:` token prefix evidence 기록. -- [ ] raw token은 review stub에 출력하지 않고 redacted 처리. - -### 테스트 작성 - -테스트 파일 추가 없음. 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:` 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 -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가 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로 기록한다. - -## [REVIEW_SMOKE_RUN-4] Manual smoke checklist evidence 정리 - -### 문제 - -`packages/messaging_flutter/README.md`의 manual smoke checklist는 FCM, notification display, ACK, opened-routing, inline reply, dismiss, token prefix를 모두 분류해야 한다. 일부만 확인하고 PASS로 처리하면 manual-smoke Epic이 과하게 완료된다. - -### 해결 방법 - -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로 분류하고 사유 기록. - -### 테스트 작성 - -테스트 파일 추가 없음. Manual evidence 정리 작업이다. - -### 중간 검증 - -```sh -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에 manual smoke checklist evidence가 항목별로 기록된다. - -## 수정 파일 요약 - -| 파일 | 항목 | -| --- | --- | -| `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 -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 -``` - -기대 결과: archived preflight dependency가 확인되고, review stub에 actual smoke checklist 항목의 PASS/BLOCKED/FAIL evidence가 있다. - -모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/flutter-test/lib/main.dart b/apps/flutter-test/lib/main.dart index eca360e5..0f5c7139 100644 --- a/apps/flutter-test/lib/main.dart +++ b/apps/flutter-test/lib/main.dart @@ -58,6 +58,7 @@ class _MyAppState extends State { Future _initializePlugin(NexoMessagingPlugin plugin) async { try { + await _configureSmokeAuthToken(plugin); await plugin.initialize(); } catch (error, stackTrace) { debugPrint('[NexoClient] Failed to initialize messaging plugin: $error'); @@ -65,6 +66,20 @@ class _MyAppState extends State { } } + Future _configureSmokeAuthToken(NexoMessagingPlugin plugin) async { + const serverUrl = String.fromEnvironment('NEXO_SMOKE_SERVER_URL'); + const authToken = String.fromEnvironment('NEXO_SMOKE_AUTH_TOKEN'); + const identifier = String.fromEnvironment('NEXO_SMOKE_SERVER_IDENTIFIER'); + if (serverUrl.isEmpty || authToken.isEmpty) return; + + await plugin.setAuthToken( + serverUrl, + authToken, + identifier: identifier.isEmpty ? null : identifier, + ); + debugPrint('[NexoClient] Smoke auth token configured'); + } + @override void dispose() { _notificationSubscription?.cancel(); diff --git a/packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt b/packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt index 8043a4b3..836899c5 100644 --- a/packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt +++ b/packages/messaging_flutter/android/src/main/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPlugin.kt @@ -3,6 +3,7 @@ package com.tokilabs.nexo.messaging import android.app.Application import android.content.Context import android.content.Intent +import android.os.Bundle import com.tokilabs.nexo.messaging.helpers.CustomPushNotificationHelper import com.tokilabs.nexo.messaging.helpers.DatabaseHelper import com.tokilabs.nexo.messaging.helpers.Network @@ -122,10 +123,7 @@ class NexoMessagingPlugin : internal fun intentPayload(intent: Intent?): Map? { val extras = intent?.extras ?: return null if (extras.isEmpty) return null - val data = mutableMapOf() - for (key in extras.keySet()) { - data[key] = extras.get(key) - } + val data = bundleToCodecMap(extras) if (data.isEmpty()) return null return data + mapOf( "type" to (data["type"] ?: "opened"), @@ -133,6 +131,43 @@ class NexoMessagingPlugin : ) } + private fun bundleToCodecMap(bundle: Bundle): Map { + val data = mutableMapOf() + for (key in bundle.keySet()) { + data[key] = codecSafeValue(bundle.get(key)) + } + return data + } + + private fun codecSafeValue(value: Any?): Any? = when (value) { + null, + is Boolean, + is Int, + is Long, + is Double, + is String, + is ByteArray, + is IntArray, + is LongArray, + is DoubleArray, + -> value + is Float -> value.toDouble() + is Short -> value.toInt() + is Char -> value.toString() + is CharSequence -> value.toString() + is Bundle -> bundleToCodecMap(value) + is BooleanArray -> value.toList() + is FloatArray -> value.map { it.toDouble() } + is ShortArray -> value.map { it.toInt() } + is CharArray -> value.map { it.toString() } + is Array<*> -> value.map { codecSafeValue(it) } + is Iterable<*> -> value.map { codecSafeValue(it) } + is Map<*, *> -> value.entries.associate { (key, entryValue) -> + key.toString() to codecSafeValue(entryValue) + } + else -> value.toString() + } + override fun onMethodCall(call: MethodCall, result: Result) { val context = applicationContext when (call.method) { diff --git a/packages/messaging_flutter/android/src/test/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPluginTest.kt b/packages/messaging_flutter/android/src/test/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPluginTest.kt index 47bc3455..7cc5a6d3 100644 --- a/packages/messaging_flutter/android/src/test/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPluginTest.kt +++ b/packages/messaging_flutter/android/src/test/kotlin/com/tokilabs/nexo/messaging/NexoMessagingPluginTest.kt @@ -188,12 +188,17 @@ internal class NexoMessagingPluginTest { // methods throw "not mocked" at runtime. We mock them with Mockito (already // a test dependency) instead of pulling in Robolectric. - private fun mockIntent(extras: Map): Intent { - val intent = Mockito.mock(Intent::class.java) + private fun mockBundle(extras: Map): Bundle { val bundle = Mockito.mock(Bundle::class.java) Mockito.`when`(bundle.isEmpty).thenReturn(extras.isEmpty()) Mockito.`when`(bundle.keySet()).thenReturn(extras.keys) extras.forEach { (key, value) -> Mockito.`when`(bundle.get(key)).thenReturn(value) } + return bundle + } + + private fun mockIntent(extras: Map): Intent { + val intent = Mockito.mock(Intent::class.java) + val bundle = mockBundle(extras) Mockito.`when`(intent.extras).thenReturn(bundle) return intent } @@ -242,6 +247,32 @@ internal class NexoMessagingPluginTest { assertEquals(true, result["userInteraction"]) } + @Test + fun intentPayload_withNestedBundle_convertsBundleToMap() { + val plugin = NexoMessagingPlugin() + val nestedBundle = mockBundle(mapOf("post_id" to "post-456")) + val intent = mockIntent(mapOf("data" to nestedBundle)) + + val result = plugin.intentPayload(intent) + + assertNotNull(result) + val data = result["data"] as Map<*, *> + assertEquals("post-456", data["post_id"]) + assertEquals("opened", result["type"]) + assertEquals(true, result["userInteraction"]) + } + + @Test + fun intentPayload_withUnsupportedExtra_stringifiesValue() { + val plugin = NexoMessagingPlugin() + val intent = mockIntent(mapOf("unsupported" to Any())) + + val result = plugin.intentPayload(intent) + + assertNotNull(result) + assertTrue(result["unsupported"] is String) + } + @Test fun intentPayload_alwaysSetsUserInteractionTrue() { val plugin = NexoMessagingPlugin()