Closer/qa/qa_push.js

72 lines
3.0 KiB
JavaScript

// qa/qa_push.js — send a REAL notification+data FCM to a user's token (faithful to the app's own
// pushes) so a KILLED app shows it in the shade and tapping it cold-starts via the real OS splash
// handover. This is the ONE path that reproduces the "opens-and-closes" crash class (e.g. the
// splash-exit iconView NPE) — `adb am start --es …` does NOT (different launch path), and
// `am force-stop` can't even receive FCM. See ClaudeQAPlan.md (Pass E crash-triage / reproduction
// fidelity) and memory/project_notifications.md.
//
// Usage: NODE_PATH=functions/node_modules node qa/qa_push.js <uid> <type> [key=value ...]
// node qa/qa_push.js <uid> partner_started_game game_type=this_or_that
// node qa/qa_push.js <uid> partner_finished_game game_type=wheel # auto-resolves a completed session
// node qa/qa_push.js <uid> chat_message conversation_id=main
// node qa/qa_push.js <uid> partner_answered
// Env overrides: SA_JSON, COUPLE_ID.
const admin = require('firebase-admin')
const path = require('path')
const SA = process.env.SA_JSON ||
path.join(__dirname, '..', 'closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json')
const COUPLE = process.env.COUPLE_ID || 'Xal3Kw3gjSdn0niERYKJ'
admin.initializeApp({ credential: admin.credential.cert(require(SA)) })
const db = admin.firestore()
const [uid, type, ...rest] = process.argv.slice(2)
if (!uid || !type) {
console.error('usage: qa_push.js <uid> <type> [key=value ...]')
process.exit(2)
}
const extras = {}
for (const kv of rest) {
const i = kv.indexOf('=')
if (i > 0) extras[kv.slice(0, i)] = kv.slice(i + 1)
}
// Channel must exist in the app (NotificationChannelSetup). game_* → game_activity, else partner_activity.
const channelId = type.includes('game') ? 'game_activity' : 'partner_activity'
;(async () => {
// A results-ready push needs a completed session id; auto-resolve one if not supplied.
if (type === 'partner_finished_game' && !extras.game_session_id && extras.game_type) {
const q = await db.collection('couples').doc(COUPLE).collection(extras.game_type).get()
let pick = null
q.forEach((d) => {
const a = d.data().answers || {}
if (Object.keys(a).length >= 2 && !pick) pick = d.id
})
if (pick) extras.game_session_id = pick
}
const tk = await db.collection('users').doc(uid).collection('fcmTokens').get()
const token = tk.docs[0] && tk.docs[0].data().token
if (!token) {
console.error('NO_TOKEN for ' + uid + ' (launch the app once so it registers a token)')
process.exit(3)
}
const data = { type, couple_id: COUPLE, ...extras }
// Distinct body so the smoke can find + tap exactly this notification (not a grouped summary).
const marker = 'QA-SMOKE:' + type
const res = await admin.messaging().send({
token,
notification: { title: 'Closer', body: marker },
android: { priority: 'high', notification: { channelId } },
data,
})
console.log('SENT ' + type + ' data=' + JSON.stringify(data) + ' -> ' + res)
process.exit(0)
})().catch((e) => {
console.error('ERR ' + e.message)
process.exit(1)
})