diff --git a/scripts/selftest.mjs b/scripts/selftest.mjs index 7e95943..4771f1f 100644 --- a/scripts/selftest.mjs +++ b/scripts/selftest.mjs @@ -3270,6 +3270,50 @@ console.log("\n㉜ 語氣層(voice/):學語氣,但不准變成照抄"); const entries = ctx.split("\n").filter((l) => l.startsWith(" - 「") || l.startsWith("遇到「")); return ctx.includes("學語氣,不要照抄這幾句") && entries.length <= pl.VOICE_INJECT_MAX; })()); + // 語域:語氣層的**加法面**。跟原句同一個目錄、同一支 voice add, + // 所以 persona-story 與 persona-anime 寫的是同一格,不會各存一份 + check("語域是第三種 voice kind,檔名與標籤都對", + pl.VOICE_KINDS.idiolect === "idiolect.md" && pl.VOICE_LABELS.idiolect === "語域"); + check("四格是寫死的,自由欄位加不進去(會長出什麼都能塞的格子)", + pl.addVoiceIdiolect("voicey", { facet: "風格", value: "文雅" }) === null); + check("CLI 用同一支 voice add 寫語域", (() => { + const res = cli(["voice", "add", "--session", S_V, "--kind", "idiolect", "--facet", "自稱", "--value", "我"]); + return res.status === 0 && fs.existsSync(pl.voicePath("voicey", "idiolect")) && + pl.loadVoice("voicey").idiolect.some((r) => r.facet === "自稱" && r.value === "我"); + })(), JSON.stringify(pl.loadVoice("voicey").idiolect)); + check("--facet 給錯會被擋,而且把四格列出來", (() => { + const res = cli(["voice", "add", "--session", S_V, "--kind", "idiolect", + "--facet", "語氣", "--value", "軟"], { expectOk: false }); + return res.status !== 0 && pl.IDIOLECT_FACETS.every((f) => String(res.stderr).includes(f)); + })()); + for (const [facet, value] of [["句尾", "⋯吧"], ["口癖", "算了"], ["不說", "絕對"]]) { + cli(["voice", "add", "--session", S_V, "--kind", "idiolect", "--facet", facet, "--value", value]); + } + check("一輪最多注入 3 格(四格輪替,不是一次全塞)", (() => { + const brief = pl.idiolectBrief("voicey", { turn: 0 }); + const parts = brief.split(":")[1].split("。")[0].split(";"); + return parts.length === pl.IDIOLECT_INJECT_MAX && brief.includes("下一輪輪到"); + })(), pl.idiolectBrief("voicey", { turn: 0 })); + check("輪替起點由第幾輪算出來,不用隨機數(同一輪重跑結果一樣)", (() => { + const a = pl.idiolectBrief("voicey", { turn: 0 }); + const b = pl.idiolectBrief("voicey", { turn: 0 }); + const c = pl.idiolectBrief("voicey", { turn: 1 }); + return a === b && a !== c; + })()); + check("四格都輪得到(跑滿一圈每格至少出現一次)", (() => { + const seen = new Set(); + for (let t = 0; t < pl.IDIOLECT_FACETS.length; t += 1) { + const brief = pl.idiolectBrief("voicey", { turn: t }); + for (const f of pl.IDIOLECT_FACETS) if (brief.includes(`${f}→`)) seen.add(f); + } + return seen.size === pl.IDIOLECT_FACETS.length; + })(), JSON.stringify(pl.IDIOLECT_FACETS.map((f, t) => pl.idiolectBrief("voicey", { turn: t }).slice(0, 60)))); + check("語域講明它是加法(黑名單刪 AI 腔,這幾格補他自己的講法)", (() => { + const brief = pl.idiolectBrief("voicey"); + return brief.includes("加法") && brief.includes("認得出是誰"); + })(), pl.idiolectBrief("voicey")); + check("turnContext 帶得到語域", pl.turnContext("voicey", S_V, "你自己怎麼說").includes("語域(這一輪")); + check("沒有語域檔的人格不注入這段", pl.idiolectBrief("calmsplit") === ""); // voice/ 會被 `sync pull`/`import` 覆蓋 → 它是外部輸入,讀取端要自己中和 check("語氣檔裡的注入標記讀進來就被中和(它是會被 sync pull 覆蓋的檔)", (() => { fs.appendFileSync(pl.voicePath("voicey", "sample"),