把正文组成收成数据契约,程序投影与旁观维护不再靠模型临场猜。

有变量真值时固定开旁观填表,开场与游玩只填模型生成区。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 23:47:58 +08:00
co-authored by Cursor
parent bb4f934b3d
commit d0a48f9431
21 changed files with 3094 additions and 224 deletions
+37 -1
View File
@@ -13,7 +13,7 @@ import {
sanitizeReasoningEffort,
sanitizeVerbosity,
} from "../src/llm/generation-compat.js";
import { buildRequestBody } from "../src/llm/client.js";
import { buildRequestBody, extractMessageParts, openRouterSessionId } from "../src/llm/client.js";
import type { LlmConfig } from "../src/config/env.js";
const stGeneration = {
@@ -410,6 +410,24 @@ describe("fetchWithGenerationCompat", () => {
});
});
describe("extractMessageParts", () => {
it("keeps reply_user when arguments are an object and id is missing", () => {
const parts = extractMessageParts({
content: [{ type: "text", text: "同一次请求里的说明" }],
tool_calls: [
{
type: "function",
function: { name: "reply_user", arguments: { text: "形成了气氛" } },
},
],
});
expect(parts.toolCalls).toEqual([
{ id: "call-0", name: "reply_user", arguments: JSON.stringify({ text: "形成了气氛" }) },
]);
expect(parts.content).toBe("同一次请求里的说明");
});
});
describe("buildRequestBody", () => {
const messages = [{ role: "user" as const, content: "hi" }];
@@ -597,6 +615,24 @@ describe("buildRequestBody", () => {
expect(body.messages).toBe(messages);
});
it("writes session_id only for OpenRouter", () => {
const openrouter: LlmConfig = {
baseUrl: "https://openrouter.ai/api/v1",
apiKey: "k",
model: "google/gemini-3.8-flash",
};
const direct: LlmConfig = {
baseUrl: "https://api.deepseek.com/v1",
apiKey: "k",
model: "deepseek-v4-pro",
};
const id = "9b76dcb6-0703-4b0d-ba75-faa985b532b4";
expect(buildRequestBody(openrouter, messages, { sessionId: id }).session_id).toBe(id);
expect(buildRequestBody(direct, messages, { sessionId: id }).session_id).toBeUndefined();
expect(buildRequestBody(openrouter, messages, {}).session_id).toBeUndefined();
expect(openRouterSessionId(openrouter.baseUrl, ` ${"a".repeat(300)} `)).toHaveLength(256);
});
it("does not forward top_k from an imported ST preset", () => {
const config: LlmConfig = {
baseUrl: "https://example.test/v1",
+162
View File
@@ -0,0 +1,162 @@
import { describe, expect, it } from "vitest";
import { Blackboard } from "../src/blackboard/blackboard.js";
import {
applyDictatePlayBind,
bindDictateProductsToPlaySpec,
} from "../src/dictate/play-bind.js";
import {
applyMaintenanceStrategyToPlaySlots,
normalizeMaintenanceAdvice,
parseMaintenanceAdviceFromVariableDesign,
resolveMaintenanceStrategyFromBoard,
strategyFromAdvice,
MAINTENANCE_STRATEGY_TAG,
} from "../src/skills/maintenance-strategy.js";
import { defaultPlaySlots } from "../src/skills/play-slots.js";
import { parseWorkerSetYaml } from "../src/skills/worker-set-parse.js";
import { VARIABLE_DESIGN_TAG } from "../src/skills/variable-design-compile.js";
function designWithAdvice(advice: string) {
return JSON.stringify({
schema: "context-fragment.v1",
技能: "变量设计与更新规则",
正文: {
真值: [
{
id: "affection",
名称: "好感",
类型: "number",
初始化: { 默认值: 0 },
公开性: "public",
},
],
维护复杂度: {
建议: advice,
依据: ["测试依据"],
},
},
});
}
describe("maintenance-strategy", () => {
it("normalizes advice aliases", () => {
expect(normalizeMaintenanceAdvice("auditor_bundle")).toBe("auditor_bundle");
expect(normalizeMaintenanceAdvice("旁观")).toBe("auditor_bundle");
expect(normalizeMaintenanceAdvice("inline_tool")).toBe("inline_tool");
expect(normalizeMaintenanceAdvice("内联")).toBe("inline_tool");
expect(normalizeMaintenanceAdvice("inline_structured")).toBe(
"inline_structured",
);
expect(normalizeMaintenanceAdvice("none")).toBe("none");
});
it("parses variable-design maintenance box", () => {
const parsed = parseMaintenanceAdviceFromVariableDesign(
designWithAdvice("auditor_bundle"),
);
expect(parsed?.advice).toBe("auditor_bundle");
expect(parsed?.依据).toContain("测试依据");
});
it("defaults to auditor_bundle when truths exist but advice missing", () => {
const raw = JSON.stringify({
schema: "context-fragment.v1",
正文: { 真值: [{ id: "a", 类型: "number", 初始化: { 默认值: 1 } }] },
});
expect(parseMaintenanceAdviceFromVariableDesign(raw)?.advice).toBe(
"auditor_bundle",
);
});
it("maps advice to auditor/inline flags at strategyFromAdvice level", () => {
expect(strategyFromAdvice("auditor_bundle")).toMatchObject({
auditor: true,
inline: false,
});
expect(strategyFromAdvice("inline_tool")).toMatchObject({
auditor: false,
inline: true,
});
expect(strategyFromAdvice("none")).toMatchObject({
auditor: false,
inline: false,
});
});
it("forces auditor_bundle when truths exist even if design said inline", () => {
const board = new Blackboard();
board.write({
tag: VARIABLE_DESIGN_TAG,
content: designWithAdvice("inline_tool"),
source: "test",
});
const strategy = resolveMaintenanceStrategyFromBoard(board);
expect(strategy.advice).toBe("auditor_bundle");
expect(strategy.auditor).toBe(true);
expect(strategy.inline).toBe(false);
expect(strategy.依据?.some((x) => x.includes("inline_tool"))).toBe(true);
});
it("keeps auditor off when there are no truths", () => {
const board = new Blackboard();
const strategy = resolveMaintenanceStrategyFromBoard(board);
expect(strategy.advice).toBe("none");
expect(strategy.auditor).toBe(false);
});
it("overrides play_slots.auditor from strategy", () => {
const slots = applyMaintenanceStrategyToPlaySlots(defaultPlaySlots(), {
schema: "maintenance-strategy.v1",
advice: "auditor_bundle",
auditor: true,
inline: false,
source: VARIABLE_DESIGN_TAG,
});
expect(slots.auditor).toBe(true);
expect(slots.gm).toBe(true);
});
});
describe("play-bind applies maintenance strategy", () => {
it("turns auditor on for auditor_bundle", () => {
const board = new Blackboard();
board.write({
tag: VARIABLE_DESIGN_TAG,
content: designWithAdvice("auditor_bundle"),
source: "test",
});
board.write({
tag: "设计.世界蓝图",
content: "舞台",
source: "test",
});
const result = bindDictateProductsToPlaySpec(board);
const parsed = parseWorkerSetYaml(result.workerSetJson);
expect(parsed?.play_slots?.auditor).toBe(true);
const strategy = resolveMaintenanceStrategyFromBoard(board);
expect(strategy.advice).toBe("auditor_bundle");
});
it("still opens auditor when design said inline_tool", () => {
const board = new Blackboard();
board.write({
tag: VARIABLE_DESIGN_TAG,
content: designWithAdvice("inline_tool"),
source: "test",
});
board.write({
tag: "设计.世界蓝图",
content: "舞台",
source: "test",
});
applyDictatePlayBind(board);
const worker = parseWorkerSetYaml(
board.getContentByTag("设计.worker集") ?? "",
);
expect(worker?.play_slots?.auditor).toBe(true);
const strategyRaw = board.getContentByTag(MAINTENANCE_STRATEGY_TAG);
expect(strategyRaw).toContain("auditor_bundle");
expect(strategyRaw).toContain('"auditor": true');
expect(strategyRaw).toContain("inline_tool");
});
});
+77
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { formatTableValue, parseTableDraft } from "../web/play-aux.js";
import { Blackboard } from "../src/blackboard/blackboard.js";
import { stringifyTableDoc, createTableFromValues } from "../src/blackboard/table-cells.js";
import { buildPlayAuxView } from "../src/skills/play-aux.js";
@@ -72,6 +73,8 @@ describe("play-aux view", () => {
expect(view.openings).toHaveLength(1);
expect(view.openings[0]?.text).toBe("@玩家站在债契前,堂屋很静。");
expect(view.openings[0]?.text).not.toContain("present.v1");
expect(view.openings[0]?.document).toContain("present.v1");
expect(view.openings[0]?.document).toContain("@玩家站在债契前,堂屋很静。");
});
it("lists dictate 设计.开场白#slot openings with same-slot 主角设定", () => {
@@ -109,6 +112,45 @@ describe("play-aux view", () => {
expect(gate?.persona?.name).toBe("阿泽");
});
it("prefers slotted 设计.开场白#场景短码 over the bare card and the legacy bundle", () => {
const board = new Blackboard();
board.write({
tag: "设计.开场白",
content: JSON.stringify({
schema: "present.v1",
shell: "prose",
blocks: { body: "这张没有短码。" },
}),
source: "agent",
});
board.write({
tag: "设计.开场白#opening-cycle2",
content: JSON.stringify({
schema: "present.v1",
shell: "status_card",
blocks: { status_card: "天数|阶段" },
}),
source: "agent",
});
board.write({
tag: "设计.开场白与开场变量",
content: JSON.stringify({
schema: "context-fragment.v1",
技能: "开场白与开场变量",
正文: { 开场白全文: "@玩家站在债契前。" },
}),
source: "agent",
});
const view = buildPlayAuxView(board);
expect(view.openings).toHaveLength(1);
expect(view.openings[0]?.tag).toBe("设计.开场白#opening-cycle2");
expect(view.openings[0]?.slot).toBe("opening-cycle2");
expect(view.openings[0]?.document).toContain("status_card");
expect(view.openings[0]?.document).toContain("天数|阶段");
// 无 body 时预览回退整包;选用仍走 document,不会压丢 status_card
expect(view.openings[0]?.document).toBe(view.openings[0]?.text);
});
it("lists proliferated zero-layer openings for play swipe", () => {
const board = new Blackboard();
board.write({
@@ -126,3 +168,38 @@ describe("play-aux view", () => {
expect(view.selectedOpeningIndex).toBe(1);
});
});
describe("play table drafts", () => {
it("keeps numbers, booleans, and JSON values in their original types", () => {
expect(parseTableDraft(1, "12")).toEqual({ ok: true, value: 12 });
expect(parseTableDraft(1, "nope").ok).toBe(false);
expect(parseTableDraft(false, "是")).toEqual({ ok: true, value: true });
expect(parseTableDraft(["基础侦察术"], '["基础侦察术","追踪"]')).toEqual({
ok: true,
value: ["基础侦察术", "追踪"],
});
expect(parseTableDraft(["基础侦察术"], "[")).toEqual({ ok: false, error: "JSON 不合法" });
expect(parseTableDraft("空", " 还在 ")).toEqual({ ok: true, value: "还在" });
});
it("pretty-prints long structured values and keeps short ones on one line", () => {
expect(formatTableValue(["基础侦察术"])).toBe('["基础侦察术"]');
const long = { a: "x".repeat(40), b: [1, 2, 3] };
expect(formatTableValue(long)).toContain("\n");
expect(formatTableValue(10)).toBe("10");
});
it("parses JSON strings so nested matrices are editable as objects", () => {
const matrix =
'{"塞琳娜":{"认知度":"有所耳闻","亲密度":-10},"崔岚":{"认知度":"有所耳闻","亲密度":-10}}';
expect(formatTableValue(matrix)).toContain("\n");
expect(formatTableValue(matrix)).toContain("塞琳娜");
expect(parseTableDraft(matrix, formatTableValue(matrix))).toEqual({
ok: true,
value: {
塞琳娜: { 认知度: "有所耳闻", 亲密度: -10 },
崔岚: { 认知度: "有所耳闻", 亲密度: -10 },
},
});
});
});
+148 -1
View File
@@ -4,6 +4,7 @@ import {
PRESENT_FRAME_SANDBOX,
buildPresentSrcdoc,
isFrontendBundle,
presentFrameSlotHtml,
presentSourceBytes,
previewPacket,
} from "../web/present-frame.js";
@@ -24,7 +25,8 @@ describe("present frame srcdoc", () => {
});
expect(srcdoc).toContain(`<div id="app"></div>`);
expect(srcdoc).toContain(`#app { color: #e1e7f0; }`);
expect(srcdoc).toContain("present.onData");
expect(srcdoc).toContain("present-host");
expect(srcdoc).toContain("applyPacket");
expect(srcdoc).toContain("示例正文");
expect(srcdoc).toContain(`default-src 'none'`);
expect(srcdoc.indexOf("window.present")).toBeLessThan(srcdoc.indexOf("data.blocks.body"));
@@ -54,6 +56,112 @@ describe("present frame srcdoc", () => {
expect(PRESENT_BYTE_LIMIT).toBe(60 * 1024);
});
it("sizes the frame to content and fills contract slots", () => {
const srcdoc = buildPresentSrcdoc({
html: `<article data-block="body"></article>`,
css: `html,body{height:100vh;overflow:auto}`,
js: `present.onData(function () {});`,
packet: { schema: "present.v1", blocks: { body: "正文", forum_feed: ["一帖"] } },
slots: [
{ id: "body", type: "markdown" },
{ id: "forum_feed", type: "list" },
],
frameId: "pf3",
});
expect(srcdoc).not.toContain("100vh");
expect(srcdoc).not.toContain("overflow:auto");
expect(srcdoc).toContain("overflow:hidden!important");
expect(srcdoc).toContain("fillSlots");
expect(srcdoc).toContain("isProgramOwned");
expect(srcdoc).toContain("forum_feed");
expect(srcdoc).toContain('"type":"list"');
});
it("turns markdown newlines into paragraphs even if the shell wrote textContent first", () => {
const srcdoc = buildPresentSrcdoc({
html: `<article id="body" data-block="body"></article>`,
css: "",
js: `present.onData(function (data) {
document.getElementById("body").textContent = data.blocks.body;
});`,
packet: {
schema: "present.v1",
blocks: { body: "第一段\\n\\n第二段 **加粗**" },
},
slots: [{ id: "body", type: "markdown" }],
frameId: "pf4",
});
// packet JSON keeps real newlines; template sample above used escaped for the test string —
// rebuild with actual newlines:
const withBreaks = buildPresentSrcdoc({
html: `<article id="body" data-block="body"></article>`,
css: "",
js: `present.onData(function (data) {
document.getElementById("body").textContent = data.blocks.body;
});`,
packet: {
schema: "present.v1",
blocks: { body: "第一段\n\n第二段 **加粗**" },
},
slots: [{ id: "body", type: "markdown" }],
frameId: "pf4b",
});
const match = withBreaks.match(/function markdown\(raw\) \{[\s\S]*?\n \}/);
expect(match).toBeTruthy();
const markdown = new Function(`${match![0]}; return markdown;`)();
const html = markdown("第一段\n\n第二段 **加粗**");
expect(html).toContain("<p>第一段</p>");
expect(html).toContain("<p>第二段 <strong>加粗</strong></p>");
expect(withBreaks).toContain("isProgramOwned");
expect(srcdoc).toContain("isProgramOwned");
});
it("applies program fill after shell onData so textContent cannot keep a wall of text", () => {
const srcdoc = buildPresentSrcdoc({
html: `<article id="body" data-block="body"></article>`,
css: "",
js: `present.onData(function (data) {
document.getElementById("body").textContent = data.blocks.body;
});`,
packet: { schema: "present.v1", blocks: { body: "a\\nb" } },
slots: [{ id: "body", type: "markdown" }],
frameId: "pf5",
});
const sealIdx = srcdoc.indexOf("present.__seal");
const ownedIdx = srcdoc.indexOf("isProgramOwned");
expect(ownedIdx).toBeGreaterThan(-1);
expect(sealIdx).toBeGreaterThan(-1);
// applyPacket: renderCb then fillSlots
const applyIdx = srcdoc.indexOf("function applyPacket");
const applyChunk = srcdoc.slice(applyIdx, applyIdx + 450);
expect(applyChunk.indexOf("renderCb")).toBeLessThan(applyChunk.lastIndexOf("fillSlots"));
expect(srcdoc).toContain('t === "kv" || t === "table" || t === "topology"');
});
it("renders list objects by text/label instead of JSON.stringify", () => {
const srcdoc = buildPresentSrcdoc({
html: `<ol data-block="options"></ol>`,
css: "",
js: `present.onData(function () {});`,
packet: { schema: "present.v1", blocks: { options: [] } },
slots: [{ id: "options", type: "list" }],
frameId: "pf6",
});
const match = srcdoc.match(/function listItemText\(item\) \{[\s\S]*?\n \}/);
expect(match).toBeTruthy();
const listItemText = new Function(`${match![0]}; return listItemText;`)();
expect(listItemText({ type: "继续推进1", text: "顺着旧话试探" })).toBe("顺着旧话试探");
expect(listItemText({ label: "换方向" })).toBe("换方向");
expect(listItemText("纯字符串")).toBe("纯字符串");
expect(listItemText('{"type":"x","text":"从字符串JSON取出"}')).toBe("从字符串JSON取出");
});
it("keeps the previous frame height so a remount does not collapse the page", () => {
expect(presentFrameSlotHtml("pf9", 1280)).toContain('style="min-height:1280px"');
expect(presentFrameSlotHtml("pf9")).not.toContain("min-height");
expect(presentFrameSlotHtml("pf9", 4)).not.toContain("min-height");
});
it("normalizes sample data into present.v1 blocks", () => {
expect(previewPacket({ schema: "present.v1", blocks: { body: "x" } })).toEqual({
schema: "present.v1",
@@ -61,4 +169,43 @@ describe("present frame srcdoc", () => {
});
expect(previewPacket(null)).toEqual({ schema: "present.v1", blocks: {} });
});
it("accepts Chinese block ids and expands nested matrix JSON into a table", () => {
const srcdoc = buildPresentSrcdoc({
html: `<section data-block="卷宗表"></section><article data-block="正文"></article>`,
css: "",
js: "present.onData(function () {});",
packet: {
schema: "present.v1",
blocks: {
卷宗表: {
source: "heroines_matrix",
rows: [
{ name: "塞琳娜", 亲密度: -10, 服从度: 10 },
{ name: "崔岚", 亲密度: -10, 服从度: 10 },
],
},
正文: "第一段\n\n第二段",
},
},
slots: [
{ id: "卷宗表", type: "table" },
{ id: "正文", type: "markdown" },
],
frameId: "pf-zh",
});
expect(srcdoc).toContain("function isBlockId");
expect(srcdoc).toContain("present-data-table");
expect(srcdoc).toContain("matrixMapToRows");
expect(srcdoc).not.toContain("/^[A-Za-z_]");
const writeMatch = srcdoc.match(/function writeDataTable\(el, rows, columns\) \{[\s\S]*?\n \}/);
expect(writeMatch).toBeTruthy();
const idMatch = srcdoc.match(/function isBlockId\(id\) \{[\s\S]*?\n \}/);
expect(idMatch).toBeTruthy();
const isBlockId = new Function(`${idMatch![0]}; return isBlockId;`)();
expect(isBlockId("正文")).toBe(true);
expect(isBlockId("body")).toBe(true);
expect(isBlockId('bad"id')).toBe(false);
});
});
+33
View File
@@ -203,6 +203,39 @@ describe("protagonist overlay", () => {
expect(protagonistTagForOpeningSlot("设计.开场白")).toBe(PROTAGONIST_TAG);
});
it("binds present.v1 meta.用户角色 to the slotted card and follows opening rewrites", async () => {
const {
ensureProtagonistFromPersona,
personaBoundToOpening,
protagonistTagForOpeningSlot,
} = await import("../src/persona/protagonist-bind.js");
const { splitOpeningDocument } = await import("../src/skills/opening-seal.js");
const openingTag = "设计.开场白#debt-hall";
const card = (简介: string) =>
JSON.stringify({
schema: "present.v1",
blocks: { body: "清晨,@玩家推开堂屋门。" },
meta: { 用户角色: { 名字: "沈聿", 简介 } },
});
const board = new Blackboard();
const personaTag = protagonistTagForOpeningSlot(openingTag);
for (const 简介 of ["本地放贷人。", "持有这家债契的放贷人。"]) {
const content = card(简介);
const split = splitOpeningDocument(content);
expect(split.persona?.name).toBe("沈聿");
expect(split.text).toBe(content);
ensureProtagonistFromPersona(board, split.persona, personaTag, {
overwrite: true,
});
board.write({ tag: openingTag, content, source: "agent" });
expect(personaBoundToOpening(board, openingTag, content)?.description).toContain(
简介,
);
}
});
it("overlays opening meta 用户角色 when the user switches to that preset", async () => {
const { readSessionProtagonist, overlayPlayPersona } = await import(
"../src/persona/protagonist-bind.js"
+88
View File
@@ -0,0 +1,88 @@
import { describe, expect, it } from "vitest";
import { parsePresentPacket } from "../src/skills/present-packet.js";
import {
openingContractError,
parseReplyDataContract,
presentSchemaFromContract,
} from "../src/skills/reply-contract.js";
const replyFormat = JSON.stringify({
schema: "context-fragment.v1",
正文: {
数据契约: {
schema: "present.v1",
blocks: {
status_card: { 类型: "kv", 来源: "program_projection", 职责: "属性" },
body: { 类型: "markdown", 来源: "model_generated", 职责: "主读正文" },
forum_feed: { 类型: "list", 来源: "model_generated", 职责: "论坛" },
danmu_stream: { 类型: "list", 来源: "model_generated", 职责: "弹幕" },
},
},
frontend: { html: `<div id="app"></div>`, css: "body{}", js: "present.onData(function(){})" },
},
});
describe("reply data contract", () => {
it("keeps model blocks and drops program projections from the fill schema", () => {
const contract = parseReplyDataContract(replyFormat);
expect(contract?.modelBlocks.map((block) => block.id)).toEqual([
"body",
"forum_feed",
"danmu_stream",
]);
expect(contract?.programBlocks.map((block) => block.id)).toEqual(["status_card"]);
expect(contract?.frontend?.html).toContain("app");
const built = presentSchemaFromContract(contract!);
const blocks = (
built.schema.properties as { blocks: { required: string[] } }
).blocks;
expect(blocks.required).toEqual(["body", "forum_feed", "danmu_stream"]);
expect(built.loose).toBe(false);
});
it("rejects an old shell packet that only fills body", () => {
const contract = parseReplyDataContract(replyFormat)!;
const error = openingContractError(
JSON.stringify({
schema: "present.v1",
shell: "prose",
blocks: { body: "清晨,@玩家推开门。" },
}),
contract,
);
expect(error).toContain("forum_feed");
expect(error).toContain("danmu_stream");
});
it("accepts present.v1 that fills every model block", () => {
const contract = parseReplyDataContract(replyFormat)!;
expect(
openingContractError(
JSON.stringify({
schema: "present.v1",
blocks: {
body: "清晨,@玩家推开门。",
forum_feed: ["有人愿意修外墙吗?"],
danmu_stream: [],
},
}),
contract,
),
).toBeUndefined();
});
it("preserves contract blocks when normalizing a present packet", () => {
const view = parsePresentPacket(
JSON.stringify({
schema: "present.v1",
blocks: {
body: "正文",
forum_feed: ["一帖"],
danmu_stream: ["一条"],
},
}),
);
expect(view.packet.blocks.forum_feed).toEqual(["一帖"]);
expect(view.packet.blocks.danmu_stream).toEqual(["一条"]);
});
});
+266
View File
@@ -0,0 +1,266 @@
import { describe, expect, it } from "vitest";
import {
catalogFieldsFromVariableDesign,
mergeCatalogFromVariableDesign,
syncVariableDesignToRuntime,
VARIABLE_DESIGN_TAG,
} from "../src/skills/variable-design-compile.js";
import {
parseVariableCatalog,
VARIABLE_CATALOG_TAG,
} from "../src/skills/variable-catalog.js";
import {
mergePresentWithProjections,
parseStatusBarProjectionBindings,
projectPresentBlocks,
} from "../src/skills/present-project.js";
import { parseReplyDataContract } from "../src/skills/reply-contract.js";
import { parsePresentPacket } from "../src/skills/present-packet.js";
import { createTableFromValues, stringifyTableDoc } from "../src/blackboard/table-cells.js";
const designRaw = JSON.stringify({
schema: "context-fragment.v1",
技能: "变量设计与更新规则",
正文: {
真值: [
{
id: "current_target_name",
名称: "当前目标",
类型: "string",
初始化: { 默认值: "沈清欢" },
公开性: "public",
},
{
id: "target_dignity",
名称: "尊严",
类型: "number",
初始化: { 默认值: 100 },
公开性: "public",
},
{
id: "secret_flag",
名称: "隐藏标记",
类型: "boolean",
初始化: { 默认值: false },
公开性: "hidden",
},
],
},
});
describe("variable-design-compile", () => {
it("parses truths into catalog fields", () => {
const fields = catalogFieldsFromVariableDesign(designRaw);
expect(fields.map((f) => f.key)).toEqual([
"current_target_name",
"target_dignity",
"secret_flag",
]);
expect(fields.find((f) => f.key === "target_dignity")?.initial).toBe(100);
expect(fields.find((f) => f.key === "secret_flag")?.user_visible).toBe(false);
});
it("merges into an existing catalog without dropping declare fields", () => {
const existing = parseVariableCatalog(
JSON.stringify({
schema: "variable-catalog.v1",
fields: [
{
key: "extra_declared",
type: "number",
initial: 1,
user_visible: true,
},
],
}),
);
const merged = mergeCatalogFromVariableDesign(existing, designRaw)!;
expect(merged.doc.fields.map((f) => f.key).sort()).toEqual([
"current_target_name",
"extra_declared",
"secret_flag",
"target_dignity",
].sort());
expect(merged.added).toEqual(
expect.arrayContaining(["current_target_name", "target_dignity", "secret_flag"]),
);
});
it("syncs design → catalog → 变量.当前", () => {
const sync = syncVariableDesignToRuntime({
designRaw,
catalogRaw: null,
currentRaw: null,
initialRaw: null,
});
expect(sync.catalog).toContain("current_target_name");
expect(sync.current).toContain("沈清欢");
expect(sync.initial).toContain("target_dignity");
expect(sync.seededKeys).toEqual(
expect.arrayContaining(["current_target_name", "target_dignity", "secret_flag"]),
);
expect(VARIABLE_DESIGN_TAG).toBe("设计.变量设计与更新规则");
expect(VARIABLE_CATALOG_TAG).toBe("设计.变量目录");
});
it("does not overwrite existing current cells when reseeding", () => {
const first = syncVariableDesignToRuntime({
designRaw,
catalogRaw: null,
currentRaw: null,
initialRaw: null,
});
const patched = createTableFromValues(
{ current_target_name: "已改名", target_dignity: 100, secret_flag: false },
"user",
);
const second = syncVariableDesignToRuntime({
designRaw,
catalogRaw: first.catalog,
currentRaw: stringifyTableDoc(patched),
initialRaw: first.initial,
});
expect(second.current).toBeNull();
});
});
describe("present-project", () => {
const replyFormat = JSON.stringify({
schema: "context-fragment.v1",
正文: {
数据契约: {
schema: "present.v1",
blocks: {
status_card: { 类型: "kv", 来源: "program_projection", 职责: "属性" },
body: { 类型: "markdown", 来源: "model_generated", 职责: "正文" },
},
},
frontend: { html: "<div id='app'></div>", css: "", js: "present.onData(function(){})" },
},
});
const statusBar = JSON.stringify({
schema: "context-fragment.v1",
正文: {
语义区域: [
{
id: "status_card",
内容类型: "kv",
来源: {
方式: "program_projection",
ref: "变量.当前",
字段: ["current_target_name", "target_dignity"],
},
},
],
},
});
it("reads projection bindings from status bar", () => {
const bindings = parseStatusBarProjectionBindings(statusBar);
expect(bindings).toEqual([
{
id: "status_card",
type: "kv",
ref: "变量.当前",
fields: ["current_target_name", "target_dignity"],
},
]);
});
it("projects selected fields into program blocks", () => {
const contract = parseReplyDataContract(replyFormat)!;
const current = stringifyTableDoc(
createTableFromValues(
{ current_target_name: "沈清欢", target_dignity: 100, other: 1 },
"system",
),
);
const projected = projectPresentBlocks({
contract,
statusBarRaw: statusBar,
currentVarsRaw: current,
});
expect(projected.status_card).toEqual({
current_target_name: "沈清欢",
target_dignity: 100,
});
});
it("falls back to all current vars when status bar has no binding", () => {
const contract = parseReplyDataContract(replyFormat)!;
const current = stringifyTableDoc(
createTableFromValues({ a: 1, b: "x" }, "system"),
);
const projected = projectPresentBlocks({
contract,
currentVarsRaw: current,
});
expect(projected.status_card).toEqual({ a: 1, b: "x" });
});
it("expands JSON-string matrices into table rows for program projection", () => {
const reply = JSON.stringify({
schema: "context-fragment.v1",
正文: {
数据契约: {
schema: "present.v1",
blocks: {
heroines: { 类型: "table", 来源: "program_projection", 职责: "矩阵" },
body: { 类型: "markdown", 来源: "model_generated", 职责: "正文" },
},
},
frontend: { html: "<div></div>", css: "", js: "present.onData(function(){})" },
},
});
const status = JSON.stringify({
schema: "context-fragment.v1",
正文: {
语义区域: [
{
id: "heroines",
内容类型: "table",
来源: {
方式: "program_projection",
ref: "变量.当前",
字段: ["heroines_matrix"],
},
},
],
},
});
const matrix = {
塞琳娜: { 认知度: "有所耳闻", 亲密度: -10, 服从度: 10 },
崔岚: { 认知度: "有所耳闻", 亲密度: -10, 服从度: 10 },
};
const current = stringifyTableDoc(
createTableFromValues(
{ heroines_matrix: JSON.stringify(matrix) },
"system",
),
);
const projected = projectPresentBlocks({
contract: parseReplyDataContract(reply)!,
statusBarRaw: status,
currentVarsRaw: current,
});
expect(projected.heroines).toEqual({
source: "heroines_matrix",
rows: [
{ name: "塞琳娜", 认知度: "有所耳闻", 亲密度: -10, 服从度: 10 },
{ name: "崔岚", 认知度: "有所耳闻", 亲密度: -10, 服从度: 10 },
],
});
});
it("merges projections into a plain body present packet", () => {
const merged = mergePresentWithProjections(
"一段正文",
{ status_card: { hp: 10 } },
"prose",
);
const view = parsePresentPacket(merged);
expect(view.packet.blocks.body).toBe("一段正文");
expect(view.packet.blocks.status_card).toEqual({ hp: 10 });
});
});