import { prisma } from "@kokorone/db"; const API_PORT = process.env.PORT_API ?? "3001"; const CHARACTER_ID = "seed-character-genki"; const TEST_USER_ID = "smoke-e-test-user"; const OTHER_USER_ID = "smoke-e-other-user"; 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}`); } 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(); } async function resetRelationship(userId, data) { await prisma.relationship.upsert({ where: { characterId_userId: { characterId: CHARACTER_ID, userId } }, update: { ...data, updatedAt: new Date() }, create: { characterId: CHARACTER_ID, userId, ...data }, }); } export default async function smokeE() { await prisma.user.upsert({ where: { id: TEST_USER_ID }, update: {}, create: { id: TEST_USER_ID, name: "關係測試使用者" }, }); await prisma.user.upsert({ where: { id: OTHER_USER_ID }, update: {}, create: { id: OTHER_USER_ID, name: "另一位測試使用者" }, }); await prisma.relationship.deleteMany({ where: { characterId: CHARACTER_ID, userId: TEST_USER_ID } }); // E-1 親密度與信任讀寫:未知對象首次互動自動建檔,預設「禮貌+防備」 const created = await get(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}`); if (created.relationship.intimacy !== 0 || created.relationship.trust !== 0 || created.relationship.stage !== "STRANGER") { throw new Error("新對象自動建檔的預設值不符「禮貌+防備」(intimacy/trust 應為 0,stage 應為 STRANGER)"); } // E-2 關係帳本與負向偏誤:同等級正負事件各一次後,淨值為負 await post(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}/events`, { event: "稱讚了她的努力", weight: 10 }); await post(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}/events`, { event: "取消約定放她鴿子", weight: -10 }); const { net } = await get(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}/ledger-net`); if (net >= 0) { throw new Error(`同等級正負事件後淨值應為負,實際為 ${net}`); } // E-3 親密度分層:連續日常互動累積親密度並跨越門檻,事件可被獨立訂閱者記錄 await resetRelationship(TEST_USER_ID, { intimacy: 0, trust: 0, stage: "STRANGER" }); for (let i = 0; i < 11; i++) { await post(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}/interactions`, {}); } const afterInteractions = await get(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}`); if (afterInteractions.relationship.stage !== "ACQUAINTANCE") { throw new Error(`累積 11 次互動(+22 親密度)後應進入認識階段,實際為 ${afterInteractions.relationship.stage}`); } const { events } = await get("/relationships/stage-events"); const crossedEvent = events.find( (e) => e.characterId === CHARACTER_ID && e.userId === TEST_USER_ID && e.fromStage === "STRANGER" && e.toStage === "ACQUAINTANCE", ); if (!crossedEvent) { throw new Error("跨越親密度分層門檻時未發出可訂閱事件(StageChangeLogService 未收到)"); } // E-4 關係時間衰減:久未互動時親密度回落,並標記「久未見」 const fortyDaysLater = new Date(Date.now() + 40 * 24 * 60 * 60 * 1000).toISOString(); const decayed = await get(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}?now=${encodeURIComponent(fortyDaysLater)}`); if (!decayed.longAbsence) { throw new Error("久未互動應標記 longAbsence 為 true"); } if (decayed.relationship.intimacy >= afterInteractions.relationship.intimacy) { throw new Error("久未互動後親密度應回落"); } // E-6 意圖推測:同一句話在高/低親密度下得到不同解讀 await resetRelationship(TEST_USER_ID, { intimacy: 80, trust: 50, stage: "BONDED" }); await resetRelationship(OTHER_USER_ID, { intimacy: 0, trust: 0, stage: "STRANGER" }); const ambiguousText = encodeURIComponent("你這個笨蛋"); const highIntimacyIntent = await get(`/relationships/${CHARACTER_ID}/${TEST_USER_ID}/intent?text=${ambiguousText}`); const lowIntimacyIntent = await get(`/relationships/${CHARACTER_ID}/${OTHER_USER_ID}/intent?text=${ambiguousText}`); if (highIntimacyIntent.tag !== "JOKE") { throw new Error(`高親密度下同一句話應解讀為玩笑,實際為 ${highIntimacyIntent.tag}`); } if (lowIntimacyIntent.tag !== "OFFENSE") { throw new Error(`低親密度下同一句話應解讀為冒犯,實際為 ${lowIntimacyIntent.tag}`); } // E-5 關係加權檢索接點:關鍵字/時間/情緒條件相同時,與當前對象相關的記憶排序優先 const now = new Date("2026-08-01T00:00:00.000Z"); await prisma.episodicMemory.upsert({ where: { id: "smoke-e5-related" }, update: { relatedUserId: TEST_USER_ID, occurredAt: now, emotionIntensity: 0.5 }, create: { id: "smoke-e5-related", characterId: CHARACTER_ID, content: "smoke-e5 測試記憶(與測試使用者相關)", occurredAt: now, emotionTag: "CALM", emotionIntensity: 0.5, source: "INTERACTION", weight: 1, relatedUserId: TEST_USER_ID, }, }); await prisma.episodicMemory.upsert({ where: { id: "smoke-e5-unrelated" }, update: { relatedUserId: null, occurredAt: now, emotionIntensity: 0.5 }, create: { id: "smoke-e5-unrelated", characterId: CHARACTER_ID, content: "smoke-e5 測試記憶(與測試使用者無關)", occurredAt: now, emotionTag: "CALM", emotionIntensity: 0.5, source: "INTERACTION", weight: 1, }, }); const withoutRelation = await get(`/memory/${CHARACTER_ID}/retrieve?query=smoke-e5&limit=2`); const relatedRankWithout = withoutRelation.results.findIndex((r) => r.id === "smoke-e5-related"); const unrelatedRankWithout = withoutRelation.results.findIndex((r) => r.id === "smoke-e5-unrelated"); if (relatedRankWithout === -1 || unrelatedRankWithout === -1) { throw new Error("E-5 測試記憶未被檢索到"); } const withRelation = await get( `/memory/${CHARACTER_ID}/retrieve?query=smoke-e5&limit=2&relatedUserId=${TEST_USER_ID}`, ); const relatedRankWith = withRelation.results.findIndex((r) => r.id === "smoke-e5-related"); const unrelatedRankWith = withRelation.results.findIndex((r) => r.id === "smoke-e5-unrelated"); if (relatedRankWith !== 0 || relatedRankWith >= unrelatedRankWith) { throw new Error("帶入 relatedUserId 後,與當前對象相關的記憶應排序優先於無關記憶"); } // 清理本次測試建立的資料,避免重複執行時累積髒資料 await prisma.episodicMemory.deleteMany({ where: { id: { in: ["smoke-e5-related", "smoke-e5-unrelated"] } } }); await prisma.relationship.deleteMany({ where: { characterId: CHARACTER_ID, userId: { in: [TEST_USER_ID, OTHER_USER_ID] } } }); }