测试与发布
在仓库根目录运行:
pnpm install
pnpm run typecheck
pnpm run test
pnpm run build
pnpm --dir docs run build
需要 Node.js 22 或以上。持久化、智能体顺序和浏览器界面修改不能只凭构建成功判断。
| 修改 | 最低附加验证 |
|---|---|
| 规范模型或世界格式 | 解析问题、往返样例、会话初始化、创作工具 |
| SQLite 架构 | 新数据库、旧版本迁移、快照/恢复、分支 |
| 记录协议 | JSON Schema、解析、原子应用、回读、记录员验证 |
| 回忆 | 角色隔离、无向量降级、向量失效、故事时间遗忘 |
| 编排 | 协议顺序、重试、取消、稳定子智能体前缀、后台 settle |
| DSH 集成 | 契约测试、热重载、聊天重开、归档/删除 |
| 浏览器界面 | 类型检查、生产构建、专用 profile 中实测 |
| 文档 | 中英文生产构建,断链视为错误 |
核心包的 tests/support/architecture.spec.ts 和状态包的 tests/architecture.spec.ts 读取源码中的导入,分层被破坏时失败:共享代码只导入自身和模型;浏览器端不导入宿主代码、Node 或状态包;宿主不导入浏览器端;包只通过根导出使用;SQLite 只经由驱动访问;工具、记录和记忆看不到驱动;模型包只导入自身。需要新规则时加在这里,而不是只在文字里描述。
两个横切测试守住单个功能测试注意不到的问题。tests/host/prompts/rules-coverage.spec.ts 对每一条某类提示必须携带的规则(只根据所给内容工作;用该聊天提示所指定的语言书写),在每种随包语言里列出携带它的提示,以及(附理由)每一个不必携带的提示;之后新增的提示不在任何一个清单里,测试会失败,直到有人作出决定。一个回合循环测试读取发给角色的每一次请求,出现键、或回合/轮次编号就失败:故事里只有名字和时间,编号属于代码。
智能体循环测试使用确定性的 fake,断言工具调用和状态变化,不断言模型文风。并发测试应主动制造重叠并验证串行化、取消和清理。失败测试应证明操作已回滚,或留下明确可恢复的状态。
脚本化模型测试看不出真实模型如何处理提示词。eval/ 会把若干场景(环顾四周并交谈、穿过出口、尝试世界规则禁止的事、等待数小时、说出角色必须记住的话)发送到正在运行的开发版 DSH,用你要评估的模型运行,再检查故事数据库、对话日志和助手记录。请使用临时的角色扮演根目录,不要用自己的:node eval/run.mjs "<带令牌的 dsh 地址>" --label qwen-35b。由模型扮演玩家的场景(soak:它用自己的话在几个回合里追求一个目标,故事必须保持完好)需要一个玩家用的模型(任何 OpenAI 兼容端点),没有时会被跳过:node eval/run.mjs "<带令牌的 dsh 地址>" --only soak --player-model <名称> [--player-url http://localhost:8081/v1] [--player-key-env 环境变量] [--player-reasoning none|omit|low|high]。玩家在各方面都有上限:最多 --player-turns 个回合(默认 6,绝不超过 30)、玩家自己的模型最多花 --player-tokens 个令牌(默认 30000)、最多 --player-minutes 分钟的墙钟时间(默认 40,并且不会开始一个按典型回合耗时会超时的回合);一次回复最多 160 个令牌,一个行动最多 300 个字符;它只收到最近一段叙述和自己最近五个行动,而不是整个故事。当它说完成、重复自己、两次没有内容可读、写出不是行动的东西(以 / 开头的命令),或回合失败、结束时没有交付任何内容时,它会停下,报告会写明原因(stop:done、turns、tokens、time、stuck、invalid、failed)。eval/player.test.mjs 用桩服务测试这些上限(node --test eval/player.test.mjs)。报告写入 eval/results/<label>.json;加上 --baseline eval/results/<之前的>.json 时,会指出哪些场景变慢超过一倍、哪些检查此前通过现在失败,并以非零状态退出。场景是 eval/scenarios/ 中的小文件,在线会话发现问题时就添加一个。
故事结束后还可以只读地事后审计:node eval/audit.mjs <会话文件夹> --url <OpenAI 兼容地址> --model <名称> [--scope story|character:<名字>|turn:<n>] [--out report.json]。评审模型阅读故事的记录(角色范围只看它参与或感知过的),并按 eval/audit-criteria.json 列出的错误(矛盾、捏造、时间线、知识泄漏、记忆遗漏、人设漂移、替玩家说话、重复)指出问题,每条都要指明所依据的记录。每个维度从 100 分起,由脚本而不是模型扣除每个有效错误的分值;没有记录编号或不在清单上的错误不计,记录不足以评判某个维度时评审弃权,分数为 null。标准只列错误、不给范文,是可编辑的文件;不给叙事质量打分。eval/audit.test.mjs 用桩服务测试评分与证据。
发布后不要修改旧 migration;新增有序迁移并测试升级。打开落后于最新架构的会话时,会先做完整性检查,并把升级前的数据库复制到数据库旁的 migration-backups/schema-<版本>.sqlite;若迁移损坏了数据,可把该副本放回为 session.sqlite。发布前检查构建产物没有私有包悬空导入,bundle 依赖及 patch 完整,包包含提示词/客户端/公开入口,中英文提示词模板及变量一致,可选 AI 能力逐项缺失时仍可运行,配置已纳入 Schemastery,并更新两种语言文档。
只在专用开发 profile 和可丢弃的世界/会话中验证,不能修改用户的普通 DSH profile。
质量门禁
拉取请求和推送到 main 时依次运行类型检查、带覆盖率的测试、发布构建和发布检查,随后进行 SonarQube 分析,其质量门禁必须通过;发布要等两者都完成。在本地生成同样的覆盖率:
pnpm test:coverage
每个包写出 packages/<name>/coverage/lcov.info,scripts/merge-coverage.mjs 把它们合并为 coverage/lcov.info,路径相对仓库根目录,也就是 sonar-project.properties 读取的文件。包内每个源文件都会列出,没有测试加载的文件按未覆盖计算,而不是缺失。测试、构建产物、示例世界、自带界面包和配置文件不参与分析或覆盖率统计。扫描需要仓库机密 SONAR_TOKEN,来自复刻仓库的拉取请求不会运行。请修复门禁报告的问题,而不是排除文件;只有测试无法运行的代码才可以添加排除,并在拉取请求中说明原因。
检查基准
世界检查有一套带标注的基准,位于 packages/dsh-agentic-roleplay-core/tests/benchmark/。它以虚构的港口小镇盐湾港为底本,中英文两个版本结构完全相同,并用到了世界格式的每一部分:有设定、声音、情绪、目标、关系、认知和日程的角色;带细节和各种状态的通路以及一把锁的地点;各种物品(隐藏的、上锁的、穿戴的、放在容器里的);有成员和目标的组织;两种范围、带日期和关联的背景;各种类型和对象的跟踪数值;主要条件与效果类型的触发器;骰子、剧情线、日历、规则、提示词和开场白。每个用例都是在底本上植入一个缺陷(或没有缺陷,或一对看似重复实则不是的内容),并写明检查必须给出的发现,除此之外不得有别的发现。
- 每次都会运行(
pnpm test):coverage.spec.ts用两种语言的底本各开始一个故事,并按功能断言世界的每一部分都进入了故事,哪一部分不再进入就会点名。cases.spec.ts运行由程序判定的用例(一致性、质量、首场景和与角色卡对照的检查),两种语言各一遍,并断言精确的发现。 - 在线运行:使用开发机上的模型,统计精确率、召回率、误报、重试、回退和耗时。
BENCH_LIVE=1 BENCH_LABEL=<名称> BENCH_MODE=llm|decision|hybrid npx vitest run tests/benchmark/live.spec.ts会运行需要模型判断的用例(数值和名称在含义层面的重复、相互矛盾的背景、设定单薄的角色、平淡的开场、缺口,以及与角色卡的对照,其中包括不应被标出的改写和翻译)。结果写入eval/results/checks-<名称>.json。BENCH_ONLY、BENCH_LANGS和BENCH_REPEAT可缩小范围或重复运行;比较差异之前请把同一用例多跑几遍,因为模型每次回答都有 随机性。 - 重排序试验:
BENCH_LIVE=1 npx vitest run tests/benchmark/rerank.spec.ts对三组带标注的数据(数值、事物、背景)里的每一对,分别按嵌入相近度和重排序模型排序,并统计真正的重复对有多少落进检查会交给模型询问的那十二对里。
不要根据未标注的世界集合去调阈值:先在这里加一个用例。