From 77d615c6b785455c4632de68e0848c2dc0294151 Mon Sep 17 00:00:00 2001 From: toki Date: Fri, 29 May 2026 16:47:09 +0900 Subject: [PATCH] update: client-validation milestone, FCM ack smoke task, flutter-test fixes, and skill updates --- HANDOFF.md | 110 ++++++++++ agent-ops/skills/common/code-review/SKILL.md | 2 + agent-ops/skills/common/plan/SKILL.md | 12 +- .../milestones/client-validation.md | 15 +- .../CODE_REVIEW-cloud-G07.md | 188 ++++++++++++++---- .../04+03_fcm_ack_smoke/PLAN-cloud-G07.md | 179 ++++++++++++----- .../plugin_integration_test.dart | 4 +- apps/flutter-test/lib/main.dart | 21 +- apps/flutter-test/pubspec.lock | 2 +- apps/flutter-test/pubspec.yaml | 1 + apps/flutter-test/test/widget_test.dart | 16 +- 11 files changed, 438 insertions(+), 112 deletions(-) create mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 00000000..b62bb752 --- /dev/null +++ b/HANDOFF.md @@ -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` diff --git a/agent-ops/skills/common/code-review/SKILL.md b/agent-ops/skills/common/code-review/SKILL.md index 2233edc9..8b8b2345 100644 --- a/agent-ops/skills/common/code-review/SKILL.md +++ b/agent-ops/skills/common/code-review/SKILL.md @@ -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 ` 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. diff --git a/agent-ops/skills/common/plan/SKILL.md b/agent-ops/skills/common/plan/SKILL.md index 151d20bf..61321d75 100644 --- a/agent-ops/skills/common/plan/SKILL.md +++ b/agent-ops/skills/common/plan/SKILL.md @@ -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: diff --git a/agent-roadmap/phase/product-foundation/milestones/client-validation.md b/agent-roadmap/phase/product-foundation/milestones/client-validation.md index ab9dc0c1..ff753278 100644 --- a/agent-roadmap/phase/product-foundation/milestones/client-validation.md +++ b/agent-roadmap/phase/product-foundation/milestones/client-validation.md @@ -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 계열은 후속으로 분리할지 결정 필요 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 index cec01c9c..2165bb9b 100644 --- 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 @@ -1,6 +1,6 @@ - + -# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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 파일 유지. --- 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 index 740049fe..2c01f198 100644 --- 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 @@ -1,6 +1,6 @@ - + -# 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:` 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:` 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/flutter-test/integration_test/plugin_integration_test.dart b/apps/flutter-test/integration_test/plugin_integration_test.dart index b807dd7f..4d448bc9 100644 --- a/apps/flutter-test/integration_test/plugin_integration_test.dart +++ b/apps/flutter-test/integration_test/plugin_integration_test.dart @@ -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 { diff --git a/apps/flutter-test/lib/main.dart b/apps/flutter-test/lib/main.dart index 3fe7c1e1..eca360e5 100644 --- a/apps/flutter-test/lib/main.dart +++ b/apps/flutter-test/lib/main.dart @@ -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 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 createState() => _MyAppState(); @@ -46,6 +51,18 @@ class _MyAppState extends State { if (!mounted) return; setState(() => _lastOpened = _formatOpened(event)); }); + if (widget.initializePlugin) { + unawaited(_initializePlugin(plugin)); + } + } + + Future _initializePlugin(NexoMessagingPlugin plugin) async { + try { + await plugin.initialize(); + } catch (error, stackTrace) { + debugPrint('[NexoClient] Failed to initialize messaging plugin: $error'); + debugPrintStack(stackTrace: stackTrace); + } } @override diff --git a/apps/flutter-test/pubspec.lock b/apps/flutter-test/pubspec.lock index 5f5f3c70..65e719b1 100644 --- a/apps/flutter-test/pubspec.lock +++ b/apps/flutter-test/pubspec.lock @@ -74,7 +74,7 @@ packages: source: hosted version: "7.0.1" firebase_core: - dependency: transitive + dependency: "direct main" description: name: firebase_core sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" diff --git a/apps/flutter-test/pubspec.yaml b/apps/flutter-test/pubspec.yaml index 85082a30..ebe542cd 100644 --- a/apps/flutter-test/pubspec.yaml +++ b/apps/flutter-test/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: flutter: sdk: flutter + firebase_core: ^3.12.1 nexo_messaging: path: ../../packages/messaging_flutter diff --git a/apps/flutter-test/test/widget_test.dart b/apps/flutter-test/test/widget_test.dart index 48263154..b589c077 100644 --- a/apps/flutter-test/test/widget_test.dart +++ b/apps/flutter-test/test/widget_test.dart @@ -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',