|
|
@@ -1,4 +1,6 @@
|
|
|
-import { describe, expect, it } from "vitest"
|
|
|
+import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
|
+import i18n from "@/i18n"
|
|
|
+import { charsPerTokenForLanguage } from "@/lib/context-budget"
|
|
|
import { annotateChapterOutlineStatus, contextPackToPrompt, trimContextPack, type ContextPack } from "./context-engine"
|
|
|
|
|
|
const basePack: ContextPack = {
|
|
|
@@ -56,6 +58,16 @@ describe("annotateChapterOutlineStatus", () => {
|
|
|
})
|
|
|
|
|
|
describe("trimContextPack 两级裁剪", () => {
|
|
|
+ // Trim-level cases were authored against the old hardcoded ×4 char quota.
|
|
|
+ // Pin English density so tokenBudget→chars stays comparable; CJK coverage
|
|
|
+ // lives in the language-density nested suite below.
|
|
|
+ beforeEach(async () => {
|
|
|
+ await i18n.changeLanguage("en")
|
|
|
+ })
|
|
|
+ afterEach(async () => {
|
|
|
+ await i18n.changeLanguage("zh")
|
|
|
+ })
|
|
|
+
|
|
|
const fullPack: ContextPack = {
|
|
|
task: "生成第10章正文",
|
|
|
chapterGoal: "主角遭遇反派,爆发冲突",
|
|
|
@@ -111,19 +123,16 @@ describe("trimContextPack 两级裁剪", () => {
|
|
|
})
|
|
|
|
|
|
it("第二级裁剪:放不下的字段做内容精简", () => {
|
|
|
+ // Minimal pack so the only overflowing field is relatedSettings — forces
|
|
|
+ // the partial-trim path instead of whole-field drops.
|
|
|
const smallPack: ContextPack = {
|
|
|
- ...fullPack,
|
|
|
- searchResults: "",
|
|
|
- graphSearchResults: "",
|
|
|
- nextChapterAdvice: "",
|
|
|
+ ...basePack,
|
|
|
+ task: "测试",
|
|
|
relatedSettings: "世界设定内容。" + "详细描述。".repeat(100),
|
|
|
- canonRules: "",
|
|
|
- writingStyle: "",
|
|
|
- timeline: "",
|
|
|
- revisionDirectives: "",
|
|
|
}
|
|
|
const result = trimContextPack(smallPack, 100)
|
|
|
expect(result.partiallyTrimmedField).toBeDefined()
|
|
|
+ expect(result.partiallyTrimmedField?.fieldKey).toBe("relatedSettings")
|
|
|
expect(result.partiallyTrimmedField?.keptChars).toBeLessThan(result.partiallyTrimmedField?.originalChars ?? 0)
|
|
|
})
|
|
|
|
|
|
@@ -172,4 +181,36 @@ describe("trimContextPack 两级裁剪", () => {
|
|
|
expect(result.prompt).not.toContain("第3章:试炼")
|
|
|
expect(result.prompt).toContain("生成第10章正文")
|
|
|
})
|
|
|
+
|
|
|
+ describe("语言密度参与字符配额", () => {
|
|
|
+ afterEach(async () => {
|
|
|
+ await i18n.changeLanguage("zh")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("同 token 预算下 CJK 字符配额约为英文的 1/4,并更早触发裁剪", async () => {
|
|
|
+ const tokenBudget = 500
|
|
|
+ // Fits English (×4 → 2000 chars) but not CJK (×1 → 500 chars).
|
|
|
+ const pack: ContextPack = {
|
|
|
+ ...basePack,
|
|
|
+ task: "测试语言密度",
|
|
|
+ soulDoc: "文".repeat(1_200),
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(charsPerTokenForLanguage("zh")).toBe(charsPerTokenForLanguage("en") / 4)
|
|
|
+
|
|
|
+ await i18n.changeLanguage("zh")
|
|
|
+ expect(charsPerTokenForLanguage()).toBe(1)
|
|
|
+ const cjk = trimContextPack(pack, tokenBudget)
|
|
|
+ expect(
|
|
|
+ cjk.trimmedFields.length + (cjk.partiallyTrimmedField ? 1 : 0),
|
|
|
+ ).toBeGreaterThan(0)
|
|
|
+
|
|
|
+ await i18n.changeLanguage("en")
|
|
|
+ expect(charsPerTokenForLanguage()).toBe(4)
|
|
|
+ const en = trimContextPack(pack, tokenBudget)
|
|
|
+ expect(en.trimmedFields).toHaveLength(0)
|
|
|
+ expect(en.partiallyTrimmedField).toBeUndefined()
|
|
|
+ expect(en.finalChars).toBeGreaterThan(cjk.finalChars)
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|