Files
Kokorone/scripts/smoke/R.mjs
T
jiantw83andClaude Sonnet 5 1bea06e3cd feat(遊戲化): 新增大廳、附身、時間流、後日談進度等遊戲化系統功能
新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、
時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙
測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 09:10:26 +08:00

110 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 } });
}