test(selftest): 補語域的案例,735 → 745

涵蓋:第三種 kind 的檔名與標籤、自由欄位加不進去、CLI 同一支寫得進去、
--facet 給錯會列出四格、一輪最多 3 格、輪替起點可重現(同一輪兩次結果相同、
不同輪不同)、跑滿一圈四格都輪得到、注入文字講明它是加法、沒有語域檔就不注入。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 08:43:48 +00:00
co-authored by Claude Opus 5
parent eebb0c0cef
commit 2352139ffc
+44
View File
@@ -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"),