import { prisma } from "@kokorone/db"; const API_PORT = process.env.PORT_API ?? "3001"; const USER_ID = "seed-user-primary"; const CHAR_ID = "smoke-q-char"; const TOKEN = "ExponentPushToken[smoke-q-test-token]"; async function post(path, body) { const res = await fetch(`http://localhost:${API_PORT}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body ?? {}), }); if (!res.ok) { throw new Error(`POST ${path} 回傳 ${res.status}:${await res.text()}`); } return res.json(); } async function get(path) { const res = await fetch(`http://localhost:${API_PORT}${path}`); if (!res.ok) { throw new Error(`GET ${path} 回傳 ${res.status}`); } return res.json(); } // Q-1/Q-2/Q-3 目前都是行動 APP(React Native + Expo,apps/mobile)消費既有的 API 契約, // 沒有為 Q 群組新增角色/對話端點——這裡驗證的是 Q 群組唯一新增的後端功能:推播通知(Q-3), // 以及 APP 賴以運作的既有契約仍然完整(/health、/chat、/characters、/schedule)。 // APP 本身的畫面/導覽/實際串接已在本群組以 `expo start --web` + 無頭瀏覽器手動驗證過 // (見 todo.md 實作記錄),不在這個純後端 smoke test 的範圍內。 export default async function smokeQ() { await prisma.character.deleteMany({ where: { id: CHAR_ID } }); await prisma.pushToken.deleteMany({ where: { token: TOKEN } }); // Q-1 驗收:APP 能呼叫 api 的 /health 並顯示結果——直接驗證這個契約存在且回應正常。 const health = await get("/health"); if (!health || typeof health.status !== "string") { throw new Error("/health 應該回傳可顯示的狀態文字"); } await post("/personality/characters", { id: CHAR_ID, source: "ORIGINAL", buildStatus: "BUILT", formalName: "測試角色", basicInfo: "測試角色", backgroundStory: "測試", personalityArchetype: "元氣", likesDislikes: "測試", goalsObsessions: "測試", speechStyle: "第一人稱「我」", initialRelationship: { userId: USER_ID, intimacy: 20, trust: 10 }, }); // Q-2 驗收(後端契約部分):聊天分頁依賴的 /chat 端點能完成一輪對話並回傳更新後的關係/情緒狀態。 const chatResult = await post(`/chat/${CHAR_ID}`, { userId: USER_ID, sessionId: "smoke-q-session", text: "嗨,最近好嗎?", }); if (!chatResult.text || !chatResult.context) { throw new Error("聊天分頁需要的一輪對話(含情緒/關係上下文)應該能正常完成"); } // Q-3 推播通知:註冊裝置 token、送出推播應該留下記錄;J 群組的提醒觸發應該會嘗試推播。 await post("/push/register", { userId: USER_ID, token: TOKEN, platform: "expo" }); const registered = await prisma.pushToken.findUnique({ where: { token: TOKEN } }); if (!registered || registered.userId !== USER_ID) { throw new Error("裝置推播 token 應該被正確註冊"); } const sendResult = await post("/push/send-test", { userId: USER_ID, title: "測試標題", body: "測試內容" }); if (sendResult.attempted !== 1 || sendResult.succeeded !== 1) { throw new Error("已註冊裝置的使用者,推播應該嘗試送出且成功(Mock Provider)"); } const log = await prisma.pushNotificationLog.findFirst({ where: { userId: USER_ID, title: "測試標題" }, orderBy: { createdAt: "desc" }, }); if (!log || !log.success) { throw new Error("每次推播嘗試都應該留下記錄"); } // 重複註冊同一個 token(模擬 APP 重啟)不應該產生重複紀錄。 await post("/push/register", { userId: USER_ID, token: TOKEN, platform: "expo" }); const tokenCount = await prisma.pushToken.count({ where: { token: TOKEN } }); if (tokenCount !== 1) { throw new Error("重複註冊同一個裝置 token 不應該產生重複紀錄"); } // J 群組整合:任務觸發時應該嘗試推播給使用者(用實際觸發一個任務來驗證,而不是直接呼叫推播端點)。 const task = await post(`/task/${CHAR_ID}`, { userId: USER_ID, type: "ONE_TIME", content: "測試提醒", rawText: "提醒我測試", }); const beforeCount = await prisma.pushNotificationLog.count({ where: { userId: USER_ID } }); await post(`/task/${task.task.id}/fire-now`, {}); const afterCount = await prisma.pushNotificationLog.count({ where: { userId: USER_ID } }); if (afterCount !== beforeCount + 1) { throw new Error("J 群組建立的提醒觸發時,應該嘗試推播給使用者的已註冊裝置"); } const taskPushLog = await prisma.pushNotificationLog.findFirst({ where: { userId: USER_ID }, orderBy: { createdAt: "desc" }, }); if (!taskPushLog?.body.includes("測試提醒")) { throw new Error("任務觸發的推播內容應該包含提醒訊息本身"); } // 清理本次測試建立的角色與推播資料(cascade 會一併清掉任務、關係、情緒狀態等)。 await prisma.character.deleteMany({ where: { id: CHAR_ID } }); await prisma.pushToken.deleteMany({ where: { token: TOKEN } }); await prisma.pushNotificationLog.deleteMany({ where: { userId: USER_ID, title: { in: ["測試標題", "心音 Kokorone"] } } }); }