@buivietphi/skill-mobile-mt 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.

package/AGENTS.md CHANGED
@@ -62,7 +62,8 @@ skill-mobile-mt/
62
62
  ├── release-checklist.md ← App Store/Play Store checklist (587 tokens)
63
63
 
64
64
  ├── offline-first.md ← Local-first + sync patterns (2,566 tokens)
65
- ├── on-device-ai.md Core ML / TFLite / llama.cpp patterns (700 tokens)
65
+ ├── testing-strategy.md Detox + Maestro + XCUITest + Espresso E2E (2,200 tokens)
66
+ ├── ci-cd.md ← GitHub Actions CI templates (2,500 tokens)
66
67
 
67
68
  ├── ── TEMPLATES (copy to your project) ────────────────────
68
69
  ├── claude-md-template.md ← CLAUDE.md for Claude Code (copy to project root)
@@ -71,7 +72,7 @@ skill-mobile-mt/
71
72
 
72
73
  **Token totals:**
73
74
  - Smart load (1 platform + core shared): **~38,600 tokens** (30.2% of 128K)
74
- - Full load (all files): **~70,000 tokens** (54.7% of 128K)
75
+ - Full load (all files): **~74,700 tokens** (58.4% of 128K)
75
76
 
76
77
  ---
77
78
 
@@ -110,7 +111,8 @@ The agent reads the task, then decides which extra file to load:
110
111
  | "Install this package / upgrade SDK" | `shared/version-management.md` |
111
112
  | "Prepare for App Store / Play Store" | `shared/release-checklist.md` |
112
113
  | "Weird issue, not sure why" | `shared/common-pitfalls.md` |
113
- | "On-device AI / ML / inference" | `shared/on-device-ai.md` |
114
+ | "Write / run E2E tests" | `shared/testing-strategy.md` |
115
+ | "Setup CI/CD / GitHub Actions" | `shared/ci-cd.md` |
114
116
 
115
117
  **Load cost:** +500 to +3,500 tokens per on-demand file.
116
118
 
@@ -145,7 +147,7 @@ The agent reads the task, then decides which extra file to load:
145
147
  ```yaml
146
148
  skill:
147
149
  name: skill-mobile-mt
148
- version: "1.4.0"
150
+ version: "1.4.2"
149
151
  author: buivietphi
150
152
  category: engineering
151
153
  tags:
@@ -178,7 +180,6 @@ skill:
178
180
  # - shared/document-analysis.md
179
181
  # - shared/release-checklist.md
180
182
  # - shared/common-pitfalls.md
181
- # - shared/on-device-ai.md
182
183
 
183
184
  project:
184
185
  description: "Read current project, adapt to its framework and conventions"
@@ -213,9 +214,9 @@ skill:
213
214
  java: ".java files in app/src/"
214
215
 
215
216
  context_budget:
216
- max_tokens: 70000
217
+ max_tokens: 74700
217
218
  smart_load_tokens: 38600
218
- savings: "~45%"
219
+ savings: "~48%"
219
220
  ```
220
221
 
221
222
  ---
@@ -261,7 +262,6 @@ Every agent MUST follow this loading sequence:
261
262
  - shared/observability.md (when adding logging, analytics, crash tracking)
262
263
  - shared/common-pitfalls.md (when encountering unfamiliar errors)
263
264
  - shared/release-checklist.md (when preparing for App Store/Play Store submission)
264
- - shared/on-device-ai.md (when adding Core ML / TFLite / on-device inference)
265
265
 
266
266
  7. SKIP non-matching platform subfolders (saves ~66% context)
267
267
  ```
@@ -283,7 +283,8 @@ Priority 6 (ON-DEMAND): shared/observability.md — Sessions as 4th pillar
283
283
  Priority 6 (ON-DEMAND): shared/document-analysis.md — Parse images/PDFs → code
284
284
  Priority 6 (ON-DEMAND): shared/release-checklist.md — Pre-release verification
285
285
  Priority 6 (ON-DEMAND): shared/common-pitfalls.md — Known issue patterns
286
- Priority 6 (ON-DEMAND): shared/on-device-ai.md — Core ML / TFLite / llama.cpp
286
+ Priority 6 (ON-DEMAND): shared/testing-strategy.md — Detox + Maestro + XCUITest + Espresso E2E
287
+ Priority 6 (ON-DEMAND): shared/ci-cd.md — GitHub Actions CI/CD templates
287
288
  ```
288
289
 
289
290
  ---
package/README.md CHANGED
@@ -232,7 +232,7 @@ iOS only?
232
232
  | SKILL.md only | ~13,200 | 10.3% | 6.6% |
233
233
  | + 1 platform + core shared/ | ~38,600 | 30.2% | 19.3% |
234
234
  | Cross-platform (RN/Flutter + iOS + Android) | ~53,000 | 41.4% | 26.5% |
235
- | All files loaded | ~70,000 | 54.7% | 35.0% |
235
+ | All files loaded | ~74,700 | 58.4% | 37.4% |
236
236
  | **Smart load (recommended)** | **~38,600** | **30.2%** | **19.3%** |
237
237
 
238
238
  ### Per-file token breakdown
@@ -259,10 +259,11 @@ iOS only?
259
259
  | `shared/version-management.md` | 3,500 |
260
260
  | `shared/observability.md` | 3,000 |
261
261
  | `shared/offline-first.md` | 2,566 |
262
- | `shared/on-device-ai.md` | 700 |
262
+ | `shared/testing-strategy.md` | 2,200 |
263
+ | `shared/ci-cd.md` | 2,500 |
263
264
  | `shared/claude-md-template.md` | ~500 |
264
265
  | `shared/agent-rules-template.md` | ~2,500 |
265
- | **Total** | **~49,500** |
266
+ | **Total** | **~53,500** |
266
267
 
267
268
  ## Installed Structure
268
269
 
@@ -294,7 +295,8 @@ iOS only?
294
295
  ├── observability.md Sessions as 4th pillar
295
296
  ├── release-checklist.md Pre-release verification
296
297
  ├── offline-first.md Local-first + sync patterns
297
- ├── on-device-ai.md Core ML / TFLite / llama.cpp
298
+ ├── testing-strategy.md Detox + Maestro + XCUITest + Espresso E2E
299
+ ├── ci-cd.md GitHub Actions CI/CD templates
298
300
  ├── claude-md-template.md CLAUDE.md template for projects
299
301
  └── agent-rules-template.md Rules templates for all agents
300
302
  ```
@@ -390,7 +392,6 @@ your-project/
390
392
  - **Anti-Pattern Detection** (`anti-patterns.md`): Detect PII leaks (CRITICAL), high cardinality tags, unbounded payloads, unstructured logs, sync telemetry on main thread — with auto-fix suggestions
391
393
  - **Performance Prediction** (`performance-prediction.md`): Calculate frame budget, FlatList bridge calls, and memory usage BEFORE writing code. Example: `50 items × 3 bridge calls × 0.3ms = 45ms/frame → 22 FPS ❌ JANK`
392
394
  - **Platform Excellence** (`platform-excellence.md`): iOS 18+ vs Android 15+ native UX standards — navigation patterns, typography, haptic feedback types, permission timing, ratings prompt flow, Live Activities/Dynamic Island, performance targets (cold start < 1s iOS, < 1.5s Android)
393
- - **On-Device AI** (`on-device-ai.md`): Decision matrix (API vs on-device), Core ML (iOS), ML Kit + MediaPipe (Android), llama.cpp cross-platform, TFLite Flutter, React Native ML Kit — with performance rules and model size guidance
394
395
  - **Version Management** (`version-management.md`): Full SDK compatibility matrix for RN 0.73-0.76, Expo 50-52, Flutter 3.22-3.27, iOS 16-18, Android 13-15. Check SDK compat BEFORE `npm install`. Release-mode testing protocol.
395
396
  - **Observability** (`observability.md`): Sessions as the 4th pillar (Metrics + Logs + Traces + **Sessions**). Session lifecycle, enrichment API, unified instrumentation stack, correlation queries. Every event carries `session_id` for full user journey reconstruction.
396
397
 
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: skill-mobile-mt
3
3
  description: "Master Senior Mobile Engineer. Patterns from 30+ production repos (200k+ GitHub stars: Ignite, Expensify, Mattermost, Immich, AppFlowy, Now in Android, TCA). Use when: building mobile features, fixing mobile bugs, reviewing mobile code, mobile architecture, React Native, Flutter, iOS Swift, Android Kotlin, mobile performance, mobile security audit, mobile code review, app release. Two modes: (1) default = pre-built production patterns, (2) 'project' = reads current project and adapts."
4
- version: "1.4.0"
4
+ version: "1.4.2"
5
5
  author: buivietphi
6
6
  priority: high
7
7
  user-invocable: true
@@ -130,8 +130,11 @@ USER REQUEST → ACTION (Read tool required)
130
130
  "Offline / cache / sync" → Read: shared/offline-first.md
131
131
  then: implement local-first architecture
132
132
 
133
- "On-device AI / ML / inference" → Read: shared/on-device-ai.md
134
- then: choose Core ML / TFLite / llama.cpp per platform
133
+ "Write/run E2E tests" → Read: shared/testing-strategy.md
134
+ then: Detox (RN) or Maestro (cross-platform) or XCUITest/Espresso
135
+
136
+ "Setup CI/CD / GitHub Actions" → Read: shared/ci-cd.md
137
+ then: test → build → distribute pipeline
135
138
 
136
139
  ```
137
140
 
package/bin/install.mjs CHANGED
@@ -70,7 +70,7 @@ const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
70
70
 
71
71
  function banner() {
72
72
  log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────────┐`);
73
- log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.0 │`);
73
+ log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.2 │`);
74
74
  log(` │ Master Senior Mobile Engineer │`);
75
75
  log(` │ │`);
76
76
  log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buivietphi/skill-mobile-mt",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 18 production apps + local project adaptation. React Native, Flutter, iOS, Android. Supports Claude, Gemini, Kimi, Cursor, Copilot, Antigravity.",
5
5
  "author": "buivietphi",
6
6
  "license": "MIT",
@@ -0,0 +1,423 @@
1
+ # CI/CD Pipelines — GitHub Actions for Mobile
2
+
3
+ > Automate: test → build → distribute. Never ship without CI.
4
+
5
+ ---
6
+
7
+ ## React Native — CI Pipeline
8
+
9
+ ```yaml
10
+ # .github/workflows/rn-ci.yml
11
+ name: React Native CI
12
+
13
+ on:
14
+ push:
15
+ branches: [main, develop]
16
+ pull_request:
17
+ branches: [main]
18
+
19
+ jobs:
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: '20'
28
+ cache: 'yarn' # or npm, pnpm
29
+
30
+ - name: Install dependencies
31
+ run: yarn install --frozen-lockfile
32
+
33
+ - name: TypeScript check
34
+ run: yarn tsc --noEmit
35
+
36
+ - name: Lint
37
+ run: yarn lint
38
+
39
+ - name: Unit tests
40
+ run: yarn test --ci --coverage --maxWorkers=2
41
+
42
+ - name: Upload coverage
43
+ uses: codecov/codecov-action@v4
44
+ with:
45
+ token: ${{ secrets.CODECOV_TOKEN }}
46
+
47
+ build-android:
48
+ runs-on: ubuntu-latest
49
+ needs: test
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - uses: actions/setup-node@v4
54
+ with:
55
+ node-version: '20'
56
+ cache: 'yarn'
57
+
58
+ - uses: actions/setup-java@v4
59
+ with:
60
+ distribution: 'temurin'
61
+ java-version: '17'
62
+
63
+ - name: Cache Gradle
64
+ uses: actions/cache@v4
65
+ with:
66
+ path: |
67
+ ~/.gradle/caches
68
+ ~/.gradle/wrapper
69
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
70
+
71
+ - name: Install dependencies
72
+ run: yarn install --frozen-lockfile
73
+
74
+ - name: Build Android APK (debug)
75
+ run: cd android && ./gradlew assembleDebug
76
+
77
+ - name: Upload APK
78
+ uses: actions/upload-artifact@v4
79
+ with:
80
+ name: app-debug.apk
81
+ path: android/app/build/outputs/apk/debug/app-debug.apk
82
+
83
+ build-ios:
84
+ runs-on: macos-15
85
+ needs: test
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+
89
+ - uses: actions/setup-node@v4
90
+ with:
91
+ node-version: '20'
92
+ cache: 'yarn'
93
+
94
+ - name: Cache CocoaPods
95
+ uses: actions/cache@v4
96
+ with:
97
+ path: ios/Pods
98
+ key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
99
+
100
+ - name: Install dependencies
101
+ run: yarn install --frozen-lockfile && cd ios && pod install
102
+
103
+ - name: Build iOS (simulator)
104
+ run: |
105
+ xcodebuild -workspace ios/MyApp.xcworkspace \
106
+ -scheme MyApp \
107
+ -sdk iphonesimulator \
108
+ -configuration Debug \
109
+ -derivedDataPath ios/build \
110
+ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
111
+ ```
112
+
113
+ ---
114
+
115
+ ## React Native — E2E with Detox
116
+
117
+ ```yaml
118
+ # .github/workflows/rn-e2e.yml
119
+ name: E2E Tests (Detox)
120
+
121
+ on:
122
+ push:
123
+ branches: [main]
124
+
125
+ jobs:
126
+ e2e-ios:
127
+ runs-on: macos-15
128
+ steps:
129
+ - uses: actions/checkout@v4
130
+
131
+ - uses: actions/setup-node@v4
132
+ with:
133
+ node-version: '20'
134
+ cache: 'yarn'
135
+
136
+ - name: Install dependencies
137
+ run: yarn install --frozen-lockfile && cd ios && pod install
138
+
139
+ - name: Install Detox CLI
140
+ run: npm install -g detox-cli
141
+
142
+ - name: Build for Detox
143
+ run: detox build --configuration ios.sim.debug
144
+
145
+ - name: Run E2E tests
146
+ run: detox test --configuration ios.sim.debug --headless
147
+
148
+ - name: Upload Detox artifacts on failure
149
+ if: failure()
150
+ uses: actions/upload-artifact@v4
151
+ with:
152
+ name: detox-artifacts
153
+ path: artifacts/
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Flutter — CI Pipeline
159
+
160
+ ```yaml
161
+ # .github/workflows/flutter-ci.yml
162
+ name: Flutter CI
163
+
164
+ on:
165
+ push:
166
+ branches: [main, develop]
167
+ pull_request:
168
+ branches: [main]
169
+
170
+ jobs:
171
+ test:
172
+ runs-on: ubuntu-latest
173
+ steps:
174
+ - uses: actions/checkout@v4
175
+
176
+ - uses: subosito/flutter-action@v2
177
+ with:
178
+ flutter-version: '3.27.x'
179
+ cache: true
180
+
181
+ - name: Install dependencies
182
+ run: flutter pub get
183
+
184
+ - name: Analyze
185
+ run: flutter analyze
186
+
187
+ - name: Unit + Widget tests
188
+ run: flutter test --coverage
189
+
190
+ - name: Upload coverage
191
+ uses: codecov/codecov-action@v4
192
+
193
+ build-android:
194
+ runs-on: ubuntu-latest
195
+ needs: test
196
+ steps:
197
+ - uses: actions/checkout@v4
198
+
199
+ - uses: actions/setup-java@v4
200
+ with:
201
+ distribution: 'temurin'
202
+ java-version: '17'
203
+
204
+ - uses: subosito/flutter-action@v2
205
+ with:
206
+ flutter-version: '3.27.x'
207
+ cache: true
208
+
209
+ - name: Install dependencies
210
+ run: flutter pub get
211
+
212
+ - name: Build APK
213
+ run: flutter build apk --debug
214
+
215
+ - name: Upload APK
216
+ uses: actions/upload-artifact@v4
217
+ with:
218
+ name: flutter-debug.apk
219
+ path: build/app/outputs/flutter-apk/app-debug.apk
220
+
221
+ build-ios:
222
+ runs-on: macos-15
223
+ needs: test
224
+ steps:
225
+ - uses: actions/checkout@v4
226
+
227
+ - uses: subosito/flutter-action@v2
228
+ with:
229
+ flutter-version: '3.27.x'
230
+ cache: true
231
+
232
+ - name: Install dependencies
233
+ run: flutter pub get
234
+
235
+ - name: Build iOS (no codesign)
236
+ run: flutter build ios --debug --no-codesign
237
+ ```
238
+
239
+ ---
240
+
241
+ ## iOS — Release to TestFlight (Fastlane)
242
+
243
+ ```yaml
244
+ # .github/workflows/ios-release.yml
245
+ name: iOS Release
246
+
247
+ on:
248
+ push:
249
+ tags: ['v*']
250
+
251
+ jobs:
252
+ release:
253
+ runs-on: macos-15
254
+ steps:
255
+ - uses: actions/checkout@v4
256
+
257
+ - uses: actions/setup-node@v4
258
+ with:
259
+ node-version: '20'
260
+ cache: 'yarn'
261
+
262
+ - name: Install dependencies
263
+ run: yarn install --frozen-lockfile && cd ios && pod install
264
+
265
+ - name: Setup certificates
266
+ uses: apple-actions/import-codesign-certs@v2
267
+ with:
268
+ p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
269
+ p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
270
+
271
+ - name: Setup provisioning profile
272
+ uses: apple-actions/download-provisioning-profiles@v1
273
+ with:
274
+ bundle-id: com.myapp
275
+ issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
276
+ api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
277
+ api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
278
+
279
+ - name: Deploy to TestFlight
280
+ run: bundle exec fastlane ios beta
281
+ env:
282
+ APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
283
+ APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
284
+ APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
285
+ ```
286
+
287
+ ```ruby
288
+ # ios/Fastfile
289
+ lane :beta do
290
+ build_app(
291
+ workspace: "MyApp.xcworkspace",
292
+ scheme: "MyApp",
293
+ configuration: "Release",
294
+ export_method: "app-store"
295
+ )
296
+ upload_to_testflight(skip_waiting_for_build_processing: true)
297
+ end
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Android — Release to Play Store (Fastlane)
303
+
304
+ ```yaml
305
+ # .github/workflows/android-release.yml
306
+ name: Android Release
307
+
308
+ on:
309
+ push:
310
+ tags: ['v*']
311
+
312
+ jobs:
313
+ release:
314
+ runs-on: ubuntu-latest
315
+ steps:
316
+ - uses: actions/checkout@v4
317
+
318
+ - uses: actions/setup-java@v4
319
+ with:
320
+ distribution: 'temurin'
321
+ java-version: '17'
322
+
323
+ - uses: actions/setup-node@v4
324
+ with:
325
+ node-version: '20'
326
+ cache: 'yarn'
327
+
328
+ - name: Install dependencies
329
+ run: yarn install --frozen-lockfile
330
+
331
+ - name: Decode keystore
332
+ run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
333
+
334
+ - name: Build release AAB
335
+ run: cd android && ./gradlew bundleRelease
336
+ env:
337
+ KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
338
+ KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
339
+ KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
340
+
341
+ - name: Deploy to Play Store
342
+ uses: r0adkll/upload-google-play@v1
343
+ with:
344
+ serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
345
+ packageName: com.myapp
346
+ releaseFiles: android/app/build/outputs/bundle/release/*.aab
347
+ track: internal
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Required GitHub Secrets
353
+
354
+ ```
355
+ # iOS
356
+ CERTIFICATES_P12 ← base64-encoded .p12 file
357
+ CERTIFICATES_P12_PASSWORD ← password for .p12
358
+ APPSTORE_KEY_ID ← App Store Connect API key ID
359
+ APPSTORE_ISSUER_ID ← App Store Connect issuer ID
360
+ APPSTORE_PRIVATE_KEY ← App Store Connect private key (.p8 contents)
361
+
362
+ # Android
363
+ KEYSTORE_BASE64 ← base64-encoded release.keystore
364
+ KEYSTORE_PASSWORD ← keystore password
365
+ KEY_ALIAS ← key alias
366
+ KEY_PASSWORD ← key password
367
+ PLAY_STORE_SERVICE_ACCOUNT_JSON ← GCP service account JSON
368
+
369
+ # Shared
370
+ CODECOV_TOKEN ← coverage reporting
371
+ MAESTRO_API_KEY ← Maestro Cloud E2E (optional)
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Caching Strategy
377
+
378
+ ```yaml
379
+ # Node modules — hash package-lock or yarn.lock
380
+ - uses: actions/cache@v4
381
+ with:
382
+ path: node_modules
383
+ key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
384
+ restore-keys: ${{ runner.os }}-node-
385
+
386
+ # Gradle — hash .gradle files
387
+ - uses: actions/cache@v4
388
+ with:
389
+ path: |
390
+ ~/.gradle/caches
391
+ ~/.gradle/wrapper
392
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
393
+
394
+ # CocoaPods — hash Podfile.lock
395
+ - uses: actions/cache@v4
396
+ with:
397
+ path: ios/Pods
398
+ key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
399
+
400
+ # Flutter pub — hash pubspec.lock
401
+ - uses: actions/cache@v4
402
+ with:
403
+ path: ~/.pub-cache
404
+ key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
405
+ ```
406
+
407
+ ---
408
+
409
+ ## Anti-Patterns
410
+
411
+ ```
412
+ ❌ Committing signing credentials to repo
413
+ ❌ Running E2E on every PR (too slow — run on main only)
414
+ ❌ No caching (3x slower builds)
415
+ ❌ Skipping unit tests before build jobs
416
+ ❌ Building on push to every branch
417
+
418
+ ✅ Store all secrets in GitHub Secrets
419
+ ✅ Cache node_modules + Gradle + CocoaPods + pub-cache
420
+ ✅ Unit tests on PR, E2E on merge to main
421
+ ✅ needs: test before build jobs
422
+ ✅ Upload build artifacts for download/QA
423
+ ```
@@ -0,0 +1,332 @@
1
+ # Mobile Testing Strategy — Unit + E2E
2
+
3
+ > Test the right things at the right layer. Don't test implementation details.
4
+
5
+ ---
6
+
7
+ ## Testing Pyramid
8
+
9
+ ```
10
+ ╱ E2E ╲ ← Detox / Maestro / XCUITest / Espresso
11
+ ╱───────╲ ← Few, slow, high-confidence
12
+ ╱Integration╲ ← API mocking, navigation flows
13
+ ╱─────────────╲ ← Medium count
14
+ ╱ Unit Tests ╲ ← Jest / XCTest / JUnit
15
+ ╱─────────────────╲ ← Many, fast, cheap
16
+
17
+ RULE: Most tests = unit. E2E = critical flows only (login, checkout, onboarding).
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Unit Tests (Jest — React Native / TypeScript)
23
+
24
+ ```typescript
25
+ // Test hooks, not components
26
+ describe('useCart', () => {
27
+ it('adds item and updates total', () => {
28
+ const { result } = renderHook(() => useCart(), { wrapper: ReduxProvider })
29
+ act(() => { result.current.addItem(mockProduct) })
30
+ expect(result.current.total).toBe(mockProduct.price)
31
+ })
32
+
33
+ it('handles addToCart API error with rollback', async () => {
34
+ server.use(rest.post('/cart', (req, res, ctx) => res(ctx.status(500))))
35
+ const { result } = renderHook(() => useCart(), { wrapper: ReduxProvider })
36
+ await act(async () => { await result.current.addItem(mockProduct) })
37
+ expect(result.current.items).toHaveLength(0) // rolled back
38
+ expect(result.current.error).toBeTruthy()
39
+ })
40
+ })
41
+
42
+ // Test Redux slices directly
43
+ describe('cartSlice', () => {
44
+ it('sets loading state on fetchCart.pending', () => {
45
+ const state = cartReducer(initialState, fetchCart.pending('', undefined))
46
+ expect(state.status).toBe('loading')
47
+ })
48
+ })
49
+ ```
50
+
51
+ **Rules:**
52
+ - Test business logic (hooks, services, slices) — NOT component layout
53
+ - Mock API calls with `msw` (Mock Service Worker)
54
+ - 4 states per feature: loading / success / error / empty
55
+
56
+ ---
57
+
58
+ ## E2E Testing — Detox (React Native)
59
+
60
+ > Best for: React Native apps, full native bridge testing.
61
+
62
+ ### Setup
63
+
64
+ ```bash
65
+ # Install
66
+ npm install --save-dev detox @config/detox
67
+
68
+ # iOS build (required before tests)
69
+ detox build --configuration ios.sim.debug
70
+
71
+ # Run tests
72
+ detox test --configuration ios.sim.debug
73
+ detox test --configuration android.emu.debug
74
+ ```
75
+
76
+ ### Config (`detox.config.js`)
77
+
78
+ ```js
79
+ module.exports = {
80
+ testRunner: {
81
+ args: { '$0': 'jest', config: 'e2e/jest.config.js' },
82
+ jest: { setupTimeout: 120000 }
83
+ },
84
+ apps: {
85
+ 'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build' },
86
+ 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug' }
87
+ },
88
+ devices: {
89
+ simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
90
+ emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } }
91
+ },
92
+ configurations: {
93
+ 'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
94
+ 'android.emu.debug': { device: 'emulator', app: 'android.debug' }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Write Detox Tests
100
+
101
+ ```typescript
102
+ // e2e/login.test.ts
103
+ describe('Login Flow', () => {
104
+ beforeAll(async () => {
105
+ await device.launchApp({ newInstance: true })
106
+ })
107
+
108
+ beforeEach(async () => {
109
+ await device.reloadReactNative()
110
+ })
111
+
112
+ it('shows error on wrong password', async () => {
113
+ await element(by.id('email-input')).typeText('user@test.com')
114
+ await element(by.id('password-input')).typeText('wrongpass')
115
+ await element(by.id('login-button')).tap()
116
+ await expect(element(by.text('Invalid credentials'))).toBeVisible()
117
+ })
118
+
119
+ it('navigates to home on success', async () => {
120
+ await element(by.id('email-input')).typeText('user@test.com')
121
+ await element(by.id('password-input')).typeText('correctpass')
122
+ await element(by.id('login-button')).tap()
123
+ await expect(element(by.id('home-screen'))).toBeVisible()
124
+ })
125
+ })
126
+ ```
127
+
128
+ **testID rules:**
129
+ ```typescript
130
+ // Add testID to interactive elements
131
+ <TextInput testID="email-input" ... />
132
+ <TouchableOpacity testID="login-button" ... />
133
+ <View testID="home-screen" ... />
134
+ ```
135
+
136
+ **What to test with Detox:**
137
+ - Login / logout flow
138
+ - Onboarding (first-time user)
139
+ - Critical purchase / checkout path
140
+ - Push notification tap → navigation
141
+ - Deep link handling
142
+
143
+ **What NOT to test with Detox:** minor UI variations, loading spinners, animations.
144
+
145
+ ---
146
+
147
+ ## E2E Testing — Maestro (Cross-Platform)
148
+
149
+ > Best for: Simpler setup, works on React Native + Flutter + native iOS/Android.
150
+
151
+ ### Setup
152
+
153
+ ```bash
154
+ # macOS
155
+ brew tap mobile-dev-inc/tap
156
+ brew install maestro
157
+
158
+ # Run a flow
159
+ maestro test e2e/login.yaml
160
+ maestro test e2e/ # all flows in folder
161
+ ```
162
+
163
+ ### Write Maestro Flows (YAML)
164
+
165
+ ```yaml
166
+ # e2e/login.yaml
167
+ appId: com.myapp
168
+ ---
169
+ - launchApp:
170
+ clearState: true
171
+
172
+ - assertVisible: "Sign In"
173
+
174
+ - tapOn:
175
+ id: "email-input"
176
+ - inputText: "user@test.com"
177
+
178
+ - tapOn:
179
+ id: "password-input"
180
+ - inputText: "wrongpass"
181
+
182
+ - tapOn:
183
+ id: "login-button"
184
+
185
+ - assertVisible: "Invalid credentials"
186
+ ```
187
+
188
+ ```yaml
189
+ # e2e/checkout.yaml
190
+ appId: com.myapp
191
+ ---
192
+ - launchApp
193
+ - tapOn: "Products"
194
+ - tapOn:
195
+ index: 0 # first product
196
+ - tapOn: "Add to Cart"
197
+ - tapOn: "Checkout"
198
+ - assertVisible: "Order Confirmed"
199
+ ```
200
+
201
+ ### Maestro Cloud CI
202
+
203
+ ```bash
204
+ # Run on real devices in Maestro Cloud
205
+ maestro cloud --apiKey $MAESTRO_API_KEY e2e/
206
+ ```
207
+
208
+ **Maestro vs Detox:**
209
+
210
+ | | Maestro | Detox |
211
+ |--|---------|-------|
212
+ | Setup | Minutes | Hours |
213
+ | YAML / Code | YAML | TypeScript |
214
+ | Cross-platform | ✅ RN + Flutter + native | RN only |
215
+ | Speed | Slower | Faster |
216
+ | Power | Medium | High |
217
+ | CI integration | Maestro Cloud | Self-hosted |
218
+
219
+ **Use Maestro when:** simple flows, cross-platform team, quick setup.
220
+ **Use Detox when:** complex interactions, React Native only, full control.
221
+
222
+ ---
223
+
224
+ ## Flutter Testing
225
+
226
+ ### Unit + Widget Tests
227
+
228
+ ```dart
229
+ // Unit test — business logic
230
+ test('CartBloc adds item correctly', () {
231
+ final bloc = CartBloc(cartRepository: MockCartRepository());
232
+ bloc.add(AddToCart(product: mockProduct));
233
+ expectLater(bloc.stream, emits(CartLoaded(items: [mockProduct])));
234
+ });
235
+
236
+ // Widget test — UI
237
+ testWidgets('ProductCard shows title and price', (tester) async {
238
+ await tester.pumpWidget(MaterialApp(
239
+ home: ProductCard(product: mockProduct),
240
+ ));
241
+ expect(find.text(mockProduct.title), findsOneWidget);
242
+ expect(find.text('\$${mockProduct.price}'), findsOneWidget);
243
+ });
244
+ ```
245
+
246
+ ### Integration Test (Flutter Driver replacement)
247
+
248
+ ```dart
249
+ // integration_test/login_test.dart
250
+ void main() {
251
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
252
+
253
+ testWidgets('login flow', (tester) async {
254
+ app.main();
255
+ await tester.pumpAndSettle();
256
+
257
+ await tester.enterText(find.byKey(Key('email')), 'user@test.com');
258
+ await tester.enterText(find.byKey(Key('password')), 'pass123');
259
+ await tester.tap(find.byKey(Key('login-button')));
260
+ await tester.pumpAndSettle();
261
+
262
+ expect(find.byKey(Key('home-screen')), findsOneWidget);
263
+ });
264
+ }
265
+ ```
266
+
267
+ ```bash
268
+ # Run on simulator
269
+ flutter test integration_test/
270
+
271
+ # Run on real device
272
+ flutter test integration_test/ -d <device-id>
273
+ ```
274
+
275
+ ---
276
+
277
+ ## iOS — XCUITest
278
+
279
+ ```swift
280
+ func testLoginFlow() throws {
281
+ let app = XCUIApplication()
282
+ app.launch()
283
+
284
+ let emailField = app.textFields["email-input"]
285
+ emailField.tap()
286
+ emailField.typeText("user@test.com")
287
+
288
+ let passwordField = app.secureTextFields["password-input"]
289
+ passwordField.tap()
290
+ passwordField.typeText("pass123")
291
+
292
+ app.buttons["login-button"].tap()
293
+
294
+ XCTAssertTrue(app.otherElements["home-screen"].waitForExistence(timeout: 5))
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Android — Espresso
301
+
302
+ ```kotlin
303
+ @Test
304
+ fun testLoginFlow() {
305
+ onView(withId(R.id.emailInput))
306
+ .perform(typeText("user@test.com"), closeSoftKeyboard())
307
+ onView(withId(R.id.passwordInput))
308
+ .perform(typeText("pass123"), closeSoftKeyboard())
309
+ onView(withId(R.id.loginButton)).perform(click())
310
+ onView(withId(R.id.homeScreen)).check(matches(isDisplayed()))
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Anti-Patterns
317
+
318
+ ```
319
+ ❌ Testing implementation details (internal state, private methods)
320
+ ❌ Testing every UI pixel (visual regression belongs in Storybook/Percy)
321
+ ❌ E2E for every edge case (unit test those)
322
+ ❌ Skipping testID on interactive elements
323
+ ❌ Running Detox without a stable test build
324
+ ❌ Using sleep() instead of waitFor()
325
+
326
+ ✅ Test user flows, not code internals
327
+ ✅ E2E for critical paths: login, purchase, onboarding
328
+ ✅ Unit test all business logic (hooks, slices, services)
329
+ ✅ Mock API calls in unit tests
330
+ ✅ Add testID to every tappable + input element
331
+ ✅ waitFor() over sleep() in Detox
332
+ ```
@@ -1,175 +0,0 @@
1
- # On-Device AI — Mobile ML Integration
2
-
3
- > On-demand. Load when: "on-device AI", "ML model", "Core ML", "TFLite", "MediaPipe", "llama", "inference", "local model"
4
- > Source: llama.cpp, Core ML, MediaPipe, TensorFlow Lite
5
-
6
- ---
7
-
8
- ## Decision Matrix
9
-
10
- ```
11
- Use case Solution
12
- ───────────────────────────────────────────────────────────
13
- Image classification / OCR Core ML (iOS) / ML Kit (Android)
14
- Text classification / sentiment Core ML NLP / ML Kit
15
- Face detection / pose estimation Vision (iOS) / MediaPipe
16
- On-device LLM chat (<7B params) llama.cpp / llama.rn / executorch
17
- Cloud LLM (>7B / latest models) API call — don't run on device
18
- Real-time object detection Core ML / TFLite + MediaPipe
19
- Speech to text (on-device) SFSpeechRecognizer (iOS) / ML Kit (Android)
20
-
21
- Rule: If model > 500MB → use API. If latency > 3s acceptable → use API.
22
- ```
23
-
24
- ---
25
-
26
- ## iOS — Core ML
27
-
28
- ```swift
29
- // 1. Import model (drag .mlpackage into Xcode)
30
- import CoreML
31
- import Vision
32
-
33
- // 2. Image classification
34
- let model = try VNCoreMLModel(for: MyClassifier(configuration: .init()).model)
35
- let request = VNCoreMLRequest(model: model) { request, _ in
36
- guard let results = request.results as? [VNClassificationObservation] else { return }
37
- let top = results.first!
38
- print("\(top.identifier): \(top.confidence)")
39
- }
40
- let handler = VNImageRequestHandler(cgImage: image, options: [:])
41
- try handler.perform([request])
42
-
43
- // 3. NLP text classification
44
- import NaturalLanguage
45
- let classifier = NLModel(mlModel: SentimentClassifier().model)
46
- let label = classifier.predictedLabel(for: "This is great!")
47
-
48
- // Model conversion: use coremltools Python package
49
- // coremltools.convert(pytorch_model, inputs=[...])
50
- ```
51
-
52
- ---
53
-
54
- ## Android — ML Kit + MediaPipe
55
-
56
- ```kotlin
57
- // ML Kit — text recognition (no model download needed)
58
- dependencies {
59
- implementation("com.google.mlkit:text-recognition:16.0.0")
60
- implementation("com.google.mlkit:face-detection:16.1.5")
61
- }
62
-
63
- val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
64
- recognizer.process(inputImage)
65
- .addOnSuccessListener { result -> result.text }
66
- .addOnFailureListener { e -> /* handle */ }
67
-
68
- // MediaPipe — pose / hand / face landmark detection
69
- dependencies {
70
- implementation("com.google.mediapipe:tasks-vision:0.10.14")
71
- }
72
-
73
- val handLandmarker = HandLandmarker.createFromOptions(context,
74
- HandLandmarkerOptions.builder()
75
- .setBaseOptions(BaseOptions.builder().setModelAssetPath("hand_landmarker.task").build())
76
- .setNumHands(2)
77
- .build()
78
- )
79
- ```
80
-
81
- ---
82
-
83
- ## On-Device LLM — llama.cpp (Cross-Platform)
84
-
85
- ```
86
- Model sizes (GGUF Q4_K_M quantization):
87
- Llama 3.2 3B → ~2GB RAM ✅ Phone-friendly
88
- Llama 3.1 8B → ~5GB RAM ⚠️ High-end only (iPhone 15 Pro, Pixel 9)
89
- Llama 3.1 70B → ~40GB RAM ❌ Not feasible on device
90
-
91
- Download: huggingface.co/models?search=gguf
92
- ```
93
-
94
- ```swift
95
- // iOS — llama.swift
96
- // https://github.com/ggerganov/llama.cpp (Swift bindings included)
97
- import llama
98
-
99
- let model = llama_load_model_from_file(modelPath, llama_model_default_params())
100
- let ctx = llama_new_context_with_model(model, llama_context_default_params())
101
- // Tokenize + run inference on background thread
102
- ```
103
-
104
- ```javascript
105
- // React Native — llama.rn
106
- // npm install llama.rn
107
- import { LlamaContext } from 'llama.rn';
108
-
109
- const context = await LlamaContext.create({
110
- model: `${RNFS.DocumentDirectoryPath}/model.gguf`,
111
- n_ctx: 2048,
112
- n_threads: 4,
113
- });
114
- const result = await context.completion({ prompt: 'Hello!', n_predict: 100 });
115
- ```
116
-
117
- ```dart
118
- // Flutter — flutter_llama (or use Platform.channel to llama.cpp)
119
- // For production: use executorch (Meta) or llama.cpp via FFI
120
- ```
121
-
122
- ---
123
-
124
- ## React Native — ML Kit (via react-native-mlkit)
125
-
126
- ```javascript
127
- // npm install @infinitered/react-native-mlkit-core
128
- // npm install @infinitered/react-native-mlkit-object-detection
129
-
130
- import { ObjectDetectionCamera } from '@infinitered/react-native-mlkit-object-detection';
131
-
132
- // Image labeling
133
- import MLKitImageLabeling from '@react-native-ml-kit/image-labeling';
134
- const labels = await MLKitImageLabeling.label(imageUri);
135
- // Returns: [{ text: 'Cat', confidence: 0.95 }]
136
- ```
137
-
138
- ---
139
-
140
- ## Flutter — tflite_flutter
141
-
142
- ```dart
143
- // pubspec.yaml: tflite_flutter: ^0.10.4
144
- import 'package:tflite_flutter/tflite_flutter.dart';
145
-
146
- final interpreter = await Interpreter.fromAsset('model.tflite');
147
- final input = [imageData]; // pre-processed tensor
148
- final output = List.filled(1000, 0).reshape([1, 1000]);
149
- interpreter.run(input, output);
150
- // output[0] = probability for each class
151
- ```
152
-
153
- ---
154
-
155
- ## Performance Rules
156
-
157
- ```
158
- 1. NEVER run inference on the main thread
159
- iOS: DispatchQueue.global(qos: .userInitiated).async { ... }
160
- Android: viewModelScope.launch(Dispatchers.Default) { ... }
161
- RN: run on JS thread or use NativeModule
162
-
163
- 2. Load model ONCE — cache in memory
164
- ❌ Load model on every inference call
165
- ✅ Load at app start or first use, keep reference
166
-
167
- 3. Batch requests when possible
168
- - Process images in background queue, not per-tap
169
-
170
- 4. Show progress for operations >500ms
171
- - Spinner or progress bar — user expects AI to take a moment
172
-
173
- 5. Fallback to API if device is low on memory
174
- let memoryPressure = ProcessInfo.processInfo.isLowPowerModeEnabled
175
- ```