新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
110 lines
4.9 KiB
JavaScript
110 lines
4.9 KiB
JavaScript
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 } });
|
||
}
|