# Mobile Push Notifications — Manual Test Plan

**Branch:** `feat/opencode-swappable-backend`  
**Date:** 2026-05-26

---

## Prerequisites

- Physical iOS or Android device (push tokens do not work on simulators)
- Gateway running locally: `npm run start` (or `npm run dev`)
- Mobile app installed via `npx expo start --tunnel` or a dev build
- Device and gateway on the same network (or using Expo tunnel)

---

## Test 1 — Token registration

**Goal:** Confirm a push token makes it into the gateway's SQLite store.

**Steps:**
1. Run the gateway: `npm run start`
2. Open the mobile app on the physical device.
3. When prompted, **grant** push notification permission.
4. Wait 5 seconds for the REST registration call to complete.

**Verify:**
```bash
sqlite3 ~/.infinibot/push_tokens.db "SELECT id, token, platform, device_label, created_at FROM push_tokens;"
```
You should see one row with your device's Expo push token, platform (`ios` or `android`), and device name.

**Also verify via REST:**
```bash
curl http://localhost:47913/api/push-tokens
```
Should return `{ "tokens": [{ "id": "...", "tokenSuffix": "...", "platform": "...", "deviceLabel": "...", ... }] }`.

---

## Test 2 — Agent completion push (app in foreground)

**Goal:** A banner appears on the device while the app is open.

**Steps:**
1. Keep the mobile app open and in the foreground.
2. In the gateway web UI or via the API, spawn a trivial claude_coder task (e.g., `echo hello`).
3. Wait for it to complete.

**Expected result:** Within ~5 seconds of task completion, a push notification banner appears at the top of the device screen with title `<sessionKey or agent label> — completed` and body `Task finished successfully.`

---

## Test 3 — Agent completion push (app closed / background)

**Goal:** Push arrives even when the app is not running.

**Steps:**
1. **Force-quit** the mobile app on the device.
2. Spawn a trivial claude_coder task from the gateway web UI.
3. Wait for it to complete.

**Expected result:** Within ~5 seconds of completion, a system push notification appears in the device notification tray. Tapping it opens the app and navigates to the Autonomy screen (or Chat if `sessionKey` is present in the payload).

---

## Test 4 — Agent failure push

**Goal:** Errors fire "high priority" pushes.

**Steps:**
1. Spawn a task configured to fail (e.g., a claude_coder task pointing at a bad directory).
2. Allow it to error.

**Expected result:** A push notification arrives with title `<label> — failed` and the `priority: "high"` flag set (on Android this plays a heads-up notification; on iOS it still arrives normally).

---

## Test 5 — Deduplication

**Goal:** A flapping event does not cause multiple pushes.

**Steps:**
1. Inspect gateway logs for `[mobile-push]` lines.
2. If the same `runId` + `phase` fires more than once within 10 minutes (e.g., a misbehaving emitter), confirm only one push is sent.

**Verify:** Search logs for the runId — you should see `sending mobile push` only once per `runId:phase` combination.

---

## Test 6 — Token deletion / logout

**Goal:** DELETE endpoint removes the token.

**Steps:**
1. Get the token ID from `GET /api/push-tokens`.
2. Run: `curl -X DELETE http://localhost:47913/api/push-tokens/<id>`
3. Verify: `GET /api/push-tokens` returns an empty array.
4. Confirm no more pushes are sent to that device.

---

## Test 7 — Permission denied banner

**Goal:** App shows the "enable notifications" banner if the user denied.

**Steps:**
1. On the device, go to **Settings → Notifications → Infinibot** and turn off notifications.
2. Kill and reopen the app.

**Expected result:** A gold banner appears at the top of the app reading *"Push notifications are off — tap to enable in Settings"*. Tapping it opens the system Settings page for the app.

---

## Test 8 — DeviceNotRegistered cleanup

**Goal:** Tokens flagged by Expo are marked invalid in the DB.

**Setup:** This requires a token that Expo's servers have invalidated (e.g., app uninstalled from device). Best tested in CI by mocking the Expo API response.

**Manual proxy:** In `src/notifications/expo-push-client.ts`, temporarily add a fake `DeviceNotRegistered` response in the batch parse, observe that `store.markInvalid()` is called and the token's `invalid_at` column is set.

---

## Files Changed

| File | Description |
|------|-------------|
| `src/notifications/push-token-store.ts` | SQLite store (`push_tokens.db`) — CRUD for push tokens |
| `src/notifications/expo-push-client.ts` | Batched HTTP wrapper around Expo Push API |
| `src/notifications/mobile-push.ts` | Agent lifecycle → push dispatcher (subscribes to `onAgentEvent`) |
| `src/gateway/server-push-tokens.ts` | REST handlers: POST register, GET list, DELETE by id |
| `src/gateway/server-http.ts` | Mounts `handlePushTokensRequest`, starts `startMobilePushDispatcher` |
| `mobile/src/services/notifications.ts` | Mobile push registration service |
| `mobile/App.js` | Calls `registerForPushNotificationsAsync` on auth, shows permission-denied banner |
