fix(emotion): loadEmotion 驗證檔案內容,衰減一律 clamp 且半衰期必須為正
emotion.json 是外部輸入(import/sync pull/手改都會進到這裡),但 loadEmotion() 用 `??=` 只補缺、不驗合法性,decayEmotion() 完全不 clamp。只要一份不合法的檔案: * baseline.joy = 5000 → decay 後 joy = 4378.79,mood 卡在 valence 100 / arousal 100 * levels.anger = "很生氣" → decay 後 anger = NaN,注入的上下文印出 valence NaN * half_life_minutes.joy ≤ 0 → factor 恆為 0,那個情緒從此累積不起來(連三次 +30 結果不變) CLI 的 --baseline 與 --apply 本來就有保護,破口純粹在檔案入口,所以擋在 loadEmotion(): levels/baseline clamp 到 0–100、非有限數字退回預設、half_life 非正數退回該情緒的預設值。 decayEmotion/decayEmotionBy 抽出共用的 applyDecay(),同樣重驗一次並 clamp 結果—— 它們是 export 的,呼叫端有可能餵手寫或匯入來的狀態,不能假設一定經過 loadEmotion()。 selftest +9:baseline 超界被 clamp、非數字不變 NaN、上下文不印 NaN、 四種壞掉的半衰期都退回預設、壞半衰期下情緒仍累積得起來、decayEmotionBy 同樣擋得住。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -843,6 +843,61 @@ console.log("\n並行寫入:read-modify-rewrite 不能吃掉同時 append 的
|
||||
!fs.existsSync(staleLock), pl.fileLockPath(victim));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// 情緒的檔案入口要驗證:emotion.json 是外部輸入(import/sync pull/手改都會進來),
|
||||
// CLI 的 --baseline/--apply 有保護,破口在檔案這一側。
|
||||
console.log("\n情緒:不合法的 emotion.json 不能污染整個狀態");
|
||||
{
|
||||
cli(["create", "--persona", "badstate", "--session", "sess-bad-8888", "--name", "Bad",
|
||||
"--creature", "壞掉的溫度計", "--vibe", "亂跳", "--emoji", "🌡"]);
|
||||
const anHourAgo = pl.iso(new Date(Date.now() - 3_600_000));
|
||||
const write = (patch) => {
|
||||
const st = pl.defaultEmotionState();
|
||||
st.updated_at = anHourAgo;
|
||||
patch(st);
|
||||
pl.writeJson(pl.emotionPath("badstate"), st);
|
||||
};
|
||||
|
||||
// ① 超出範圍的 baseline:以前 decay 後 joy = 4378,mood 卡在 valence 100 / arousal 100
|
||||
write((st) => { st.baseline.joy = 5000; });
|
||||
const over = pl.decayEmotion(pl.loadEmotion("badstate"));
|
||||
check("baseline 超出 0–100 會被 clamp(不會把 mood 頂到滿)",
|
||||
over.baseline.joy === 100 && over.levels.joy <= 100 && pl.mood(over).valence < 100,
|
||||
`baseline=${over.baseline.joy} joy=${over.levels.joy} valence=${pl.mood(over).valence}`);
|
||||
|
||||
// ② 非數字的 levels:以前 decay 後 anger = NaN,注入的上下文直接印出 valence NaN
|
||||
write((st) => { st.levels.anger = "很生氣"; st.levels.joy = null; });
|
||||
const nan = pl.decayEmotion(pl.loadEmotion("badstate"));
|
||||
check("非數字的情緒值退回預設,不會變成 NaN",
|
||||
Number.isFinite(nan.levels.anger) && Number.isFinite(nan.levels.joy), JSON.stringify(nan.levels));
|
||||
check("注入的上下文不會印出 NaN",
|
||||
!pl.emotionBrief("badstate", nan).includes("NaN") && Number.isFinite(pl.mood(nan).valence),
|
||||
pl.emotionBrief("badstate", nan));
|
||||
|
||||
// ③ 半衰期 ≤ 0:以前 factor 恆為 0,那個情緒從此累積不起來(連三次 +30 結果都一樣)
|
||||
for (const bad of [-10, 0, "很久", null]) {
|
||||
write((st) => { st.half_life_minutes.joy = bad; });
|
||||
check(`half_life=${JSON.stringify(bad)} 會退回預設半衰期`,
|
||||
pl.loadEmotion("badstate").half_life_minutes.joy === pl.EMOTIONS.joy.halfLife);
|
||||
}
|
||||
write((st) => { st.half_life_minutes.joy = -10; });
|
||||
const grow = [];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
cli(["emotion", "--session", "sess-bad-8888", "--apply", "joy=+30"]);
|
||||
grow.push(pl.readJson(pl.emotionPath("badstate")).levels.joy);
|
||||
}
|
||||
check("半衰期壞掉時情緒仍然累積得起來(以前三次 +30 一動也不動)",
|
||||
grow[1] > grow[0] && grow[2] > grow[1], grow.join(" → "));
|
||||
|
||||
// ④ 睡眠的固定衰減走同一條路
|
||||
write((st) => { st.half_life_minutes.sadness = 0; st.levels.sadness = 90; });
|
||||
const slept = pl.decayEmotionBy(pl.loadEmotion("badstate"), pl.SLEEP_DECAY_MINUTES);
|
||||
check("decayEmotionBy 也擋得住壞掉的半衰期(不會一覺把情緒歸零)",
|
||||
slept.levels.sadness > pl.DEFAULT_BASELINE.sadness && slept.levels.sadness < 90,
|
||||
`sadness=${slept.levels.sadness}`);
|
||||
cli(["release", "--session", "sess-bad-8888"]);
|
||||
}
|
||||
|
||||
console.log("\n情緒:飽和、單輪預算、走向、偏差稽核");
|
||||
let sat = pl.defaultEmotionState();
|
||||
for (let i = 0; i < 10; i += 1) sat = pl.applyEmotion(sat, { joy: +20 }, "連灌");
|
||||
|
||||
Reference in New Issue
Block a user