import { prisma } from "@kokorone/db"; const API_PORT = process.env.PORT_API ?? "3001"; const CHARACTER_ID = "seed-character-genki"; 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 countLongTermMemories() { const [episodic, semantic, procedural] = await Promise.all([ prisma.episodicMemory.count(), prisma.semanticMemory.count(), prisma.proceduralRule.count(), ]); return episodic + semantic + procedural; } export default async function smokeC() { const sessionId = `smoke-c-${Date.now()}`; const highEmotionContent = `我剛剛收到夢寐以求的錄取通知,超級感動![${sessionId}]`; const countBeforeAppend = await countLongTermMemories(); // C-1 工作記憶溢位裁切:附加夠多低情緒內容觸發裁切,驗證高情緒段落被保留 const filler = "今天天氣普通普通的沒什麼特別事情發生。"; for (let i = 0; i < 5; i++) { await post(`/memory/${CHARACTER_ID}/sessions/${sessionId}/messages`, { role: "user", content: `${filler.repeat(6)}${i}`, emotionTag: "CALM", emotionIntensity: 0.05, }); } await post(`/memory/${CHARACTER_ID}/sessions/${sessionId}/messages`, { role: "user", content: highEmotionContent, emotionTag: "JOY", emotionIntensity: 0.95, }); const { context: afterOverflow } = await get(`/memory/${CHARACTER_ID}/sessions/${sessionId}/messages`); if (!afterOverflow.some((m) => m.content === highEmotionContent)) { throw new Error("工作記憶溢位裁切錯誤地清掉了高情緒段落"); } if (afterOverflow.length >= 6) { throw new Error("工作記憶溢位裁切未生效,段落數未減少"); } // C-2 對話期只寫工作記憶:附加訊息期間長期記憶表筆數不應變動 const countAfterAppend = await countLongTermMemories(); if (countAfterAppend !== countBeforeAppend) { throw new Error("對話期間長期記憶表筆數發生變化,違反「對話中不即時寫長期記憶」"); } // C-4 睡眠固化:只有高情緒事件進 EpisodicMemory,瑣事不進 await post(`/memory/${CHARACTER_ID}/sessions/${sessionId}/consolidate`); const { context: afterConsolidate } = await get(`/memory/${CHARACTER_ID}/sessions/${sessionId}/messages`); if (afterConsolidate.length !== 0) { throw new Error("固化後工作記憶未清空"); } const countAfterConsolidate = await countLongTermMemories(); if (countAfterConsolidate !== countBeforeAppend + 1) { throw new Error( `固化後長期記憶筆數變化不符預期(預期 +1,實際 ${countAfterConsolidate - countBeforeAppend})`, ); } // C-6 記憶檢索器:關鍵字命中、瑣事未污染結果 const { results: retrieved } = await get( `/memory/${CHARACTER_ID}/retrieve?query=${encodeURIComponent("錄取通知")}`, ); const consolidatedMemory = retrieved.find((m) => m.content === highEmotionContent); if (!consolidatedMemory) { throw new Error("高情緒事件未寫入 EpisodicMemory 或檢索不到"); } if (retrieved.some((m) => m.content.startsWith(filler))) { throw new Error("瑣事錯誤地被寫入 EpisodicMemory"); } // C-5 提取即改寫:檢索命中後提取次數遞增 const { results: retrievedAgain } = await get( `/memory/${CHARACTER_ID}/retrieve?query=${encodeURIComponent("錄取通知")}`, ); const secondHit = retrievedAgain.find((m) => m.id === consolidatedMemory.id); if (!secondHit || secondHit.retrievalCount <= consolidatedMemory.retrievalCount) { throw new Error("重複檢索未持續遞增提取次數"); } // C-5 遺忘:模擬時間推進,高情緒記憶不應被衰減 const twoDaysLater = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(); await post("/memory/forgetting-sweep", { now: twoDaysLater }); const { results: afterSweep } = await get( `/memory/${CHARACTER_ID}/retrieve?query=${encodeURIComponent("錄取通知")}`, ); const highEmotionAfterSweep = afterSweep.find((m) => m.id === consolidatedMemory.id); if (!highEmotionAfterSweep || highEmotionAfterSweep.weight < secondHit.weight) { throw new Error("遺忘程序錯誤地影響了高情緒記憶"); } // 清理本次測試寫入的長期記憶,避免重複執行時累積髒資料 await prisma.episodicMemory.delete({ where: { id: consolidatedMemory.id } }); // C-3 排程抽象層:schedule(at) 排入的工作在時間到之前不觸發、時間到之後準時觸發 const jobId = `smoke-c-schedule-${Date.now()}`; await post("/memory/test-scheduled-job", { id: jobId, delaySeconds: 2 }); const { fired: firedTooEarly } = await get(`/memory/test-scheduled-job/${jobId}`); if (firedTooEarly) { throw new Error("schedule(at) 排入的工作在時間到之前就觸發了"); } await new Promise((resolve) => setTimeout(resolve, 3000)); const { fired: firedOnTime } = await get(`/memory/test-scheduled-job/${jobId}`); if (!firedOnTime) { throw new Error("schedule(at) 排入的工作逾時未觸發"); } }