import { prisma } from "@kokorone/db"; const API_PORT = process.env.PORT_API ?? "3001"; const USER_ID = "smoke-r-user"; const CHAR_A = "smoke-r-char-a"; const CHAR_B = "smoke-r-char-b"; 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}:${await res.text()}`); } return res.json(); } async function createCharacter(id, formalName) { await post("/personality/characters", { id, source: "ORIGINAL", buildStatus: "BUILT", formalName, basicInfo: `${formalName},測試角色`, backgroundStory: "測試用背景", personalityArchetype: "元氣", likesDislikes: "測試", goalsObsessions: "測試", speechStyle: "第一人稱「我」", initialRelationship: { userId: USER_ID, intimacy: 50, trust: 50 }, }); } // P1 端到端驗證:建房與取回、房間持久化(供重啟後接續推進的依據)、大廳常駐房在場流轉、 // 離開後重進、1:1 併入 SOLO 房後關房仍觸發睡眠固化。 export default async function smokeR() { await prisma.room.deleteMany({ where: { userId: USER_ID } }); await prisma.character.deleteMany({ where: { id: { in: [CHAR_A, CHAR_B] } } }); await prisma.user.upsert({ where: { id: USER_ID }, update: {}, create: { id: USER_ID, name: "R 群組測試使用者" } }); await createCharacter(CHAR_A, "小R甲"); await createCharacter(CHAR_B, "小R乙"); // T3/T12 驗收:建房與取回。 const room = await post("/room", { userId: USER_ID, characterIds: [CHAR_A, CHAR_B], mode: "GROUP" }); const fetched = await get(`/room/${room.id}`); if (fetched.id !== room.id || fetched.participants.length !== 2) { throw new Error("建房後應能立即取回相同房間與完整在場名單"); } // T37 驗收:對話會更新 lastAdvancedAt——這是重啟後排程據以判斷「是否該立刻接續推進」的依據; // 資料直接落在 DB(非行程記憶體),故任何重新連線(含 api 重啟後的新行程)都能讀到同一筆值。 // 「api 行程真的重啟後房間仍能取回」已於 T37 由人工實際重啟驗證覆蓋(見該項備註的 6 小時停機模擬)。 await post(`/room/${room.id}/message`, { userId: USER_ID, userLabel: "測試", text: "哈囉大家" }); const roomRow = await prisma.room.findUnique({ where: { id: room.id } }); if (!roomRow?.lastAdvancedAt) { throw new Error("推進後 lastAdvancedAt 應被更新並持久化,供重啟恢復判斷使用"); } // T24 驗收:大廳常駐房在場流轉——進房後大廳在場名單少一人。 const lobbyBefore = await get(`/room/lobby?userId=${USER_ID}`); if (!lobbyBefore.roster.includes(CHAR_A)) { throw new Error("尚未進房前,角色應在大廳在場名單中"); } const soloRoom = await post("/room/solo", { userId: USER_ID, characterId: CHAR_A }); const lobbyAfterEnter = await get(`/room/lobby?userId=${USER_ID}`); if (lobbyAfterEnter.roster.includes(CHAR_A)) { throw new Error("進房後大廳在場名單應該少一人"); } // T12 驗收:離開後重進,對話歷程完整且角色仍在場。 await post(`/room/${soloRoom.id}/message`, { userId: USER_ID, text: "你好呀" }); const reentered = await get(`/room/${soloRoom.id}`); if (reentered.turns.length === 0) { throw new Error("重新進入房間應能看到先前對話歷程"); } if (!reentered.participants.includes(CHAR_A)) { throw new Error("重新進入房間時角色應仍在場"); } // T38 驗收:1:1 併入 SOLO 房後,關房仍觸發睡眠固化(工作記憶清空)並自動回到大廳。 const workingMemBefore = await get(`/memory/${CHAR_A}/sessions/${soloRoom.id}/messages`); if (workingMemBefore.context.length === 0) { throw new Error("關房前工作記憶應保留本輪對話內容"); } await post(`/room/${soloRoom.id}/end`, {}); const workingMemAfter = await get(`/memory/${CHAR_A}/sessions/${soloRoom.id}/messages`); if (workingMemAfter.context.length !== 0) { throw new Error("關房應觸發睡眠固化並清空工作記憶"); } const lobbyAfterLeave = await get(`/room/lobby?userId=${USER_ID}`); if (!lobbyAfterLeave.roster.includes(CHAR_A)) { throw new Error("關房後角色應自動回到大廳"); } // 清理本次測試建立的角色、房間與使用者。 await prisma.room.deleteMany({ where: { userId: USER_ID } }); await prisma.character.deleteMany({ where: { id: { in: [CHAR_A, CHAR_B] } } }); await prisma.user.deleteMany({ where: { id: USER_ID } }); }