feat(gateway): 配置只读事实面 + 后台思考行「档位/穿透」(契约 v0.68)
- 新增 GET /api/v1/admin/config-facts:记忆整理 7 键 + 安静时段 2 键的**进程生效值** + 声明默认值(反射读 default 标签,不读 .env) - 面板:记忆整理卡 + 安静时段卡(Miuix 只读),文案按新语义「只服务主动搭话,不静默回复」 - 后台思考行加 urgency(**只能是下限**,存档不可分 high/critical)+ quietHoursBypassed(与既有 bypassed 是两件事) - 红线实测:往响应里塞 JWT ⇒ TestAdminConfigFacts_NeverLeaksSecretValues 打红(连断言消息都把整个响应打出来了) - 前端:configFacts 24 checks / ConfigFactsCards 29 / BackthinkUrgencyTags 19 全过 - 契约 v0.68(纯增量:B12 新端点 + B8 两字段 + §4.10 形状与语义 + §7.4-17 下限限定)
This commit is contained in:
1 parent
a057a0f61f
commit
34cef95e17
25 files changed
+1741
-14
No files matched your search
@@ -0,0 +1,137 @@
|
||||
// 配置只读事实面(契约 v0.68 §4.10(a))的**纯逻辑**测试:形状 → 文案的映射。
|
||||
// 这里盯的是三条不变量:值只取后端给的生效值、默认/已改只由 isDefault 决定、
|
||||
// 后端没给的键如实显示「未报告」而不是编一个 0。
|
||||
//
|
||||
// 跑法(项目无前端测试运行器,用 vite 自带的 esbuild;见同目录既有测试):
|
||||
//
|
||||
// cd frontend/web \
|
||||
// && ./node_modules/.bin/esbuild src/admin/modules/configFacts.test.ts \
|
||||
// --bundle --platform=node --format=esm \
|
||||
// --outfile=/tmp/cyrene-config-facts.test.mjs \
|
||||
// && node /tmp/cyrene-config-facts.test.mjs
|
||||
//
|
||||
// `tsc --noEmit`(npm run build 的一部分)会类型检查本文件,因此它不 import node:*。
|
||||
|
||||
import type { AdminConfigFacts } from "../../lib/types";
|
||||
import {
|
||||
FACT_MISSING_VALUE,
|
||||
MEMORY_TIDY_KEYS,
|
||||
QUIET_HOURS_KEYS,
|
||||
factRows,
|
||||
memoryTidyRows,
|
||||
quietHoursPurpose,
|
||||
quietHoursRows,
|
||||
quietHoursSummary,
|
||||
} from "./configFacts";
|
||||
|
||||
let checks = 0;
|
||||
|
||||
function ok(cond: boolean, what: string): void {
|
||||
checks += 1;
|
||||
if (!cond) throw new Error(`FAILED: ${what}`);
|
||||
}
|
||||
|
||||
function same<T>(got: T, want: T, what: string): void {
|
||||
ok(
|
||||
JSON.stringify(got) === JSON.stringify(want),
|
||||
`${what} — got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function has(haystack: string, needle: string): boolean {
|
||||
return haystack.includes(needle);
|
||||
}
|
||||
|
||||
/** 生效值全为默认的常态响应(后端在默认部署下会这么回)。 */
|
||||
const DEFAULTS: AdminConfigFacts = {
|
||||
facts: [
|
||||
{ key: "MEMORY_TIDY_ENABLED", value: "true", default: "true", isDefault: true },
|
||||
{ key: "MEMORY_TIDY_INTERVAL_SEC", value: "86400", default: "86400", isDefault: true },
|
||||
{ key: "MEMORY_TIDY_QUIET_START_HOUR", value: "23", default: "23", isDefault: true },
|
||||
{ key: "MEMORY_TIDY_QUIET_END_HOUR", value: "7", default: "7", isDefault: true },
|
||||
{ key: "MEMORY_TIDY_RECALL_THRESHOLD", value: "0.8", default: "0.8", isDefault: true },
|
||||
{ key: "MEMORY_TIDY_SCAN_LIMIT", value: "5000", default: "5000", isDefault: true },
|
||||
{ key: "MEMORY_TIDY_CANDIDATE_LIMIT", value: "20", default: "20", isDefault: true },
|
||||
{ key: "QUIET_START_HOUR", value: "23", default: "23", isDefault: true },
|
||||
{ key: "QUIET_END_HOUR", value: "7", default: "7", isDefault: true },
|
||||
],
|
||||
quietHours: { startHour: 23, endHour: 7, disabled: false, label: "23:00-07:00" },
|
||||
};
|
||||
|
||||
// ── ① 值就是后端给的生效值(含单位),不重算 ─────────────────────────────────
|
||||
const tidy = memoryTidyRows(DEFAULTS);
|
||||
same(tidy.length, 7, "记忆整理恰好 7 行(后端的 7 个键)");
|
||||
same(
|
||||
tidy.map((r) => r.key),
|
||||
[...MEMORY_TIDY_KEYS],
|
||||
"键序与后端事实表一致",
|
||||
);
|
||||
const interval = tidy.find((r) => r.key === "MEMORY_TIDY_INTERVAL_SEC");
|
||||
same(interval?.value, "86400 秒", "周期带单位显示,数值原样取后端");
|
||||
same(interval?.state, "默认", "isDefault=true ⇒ 显示「默认」");
|
||||
const threshold = tidy.find((r) => r.key === "MEMORY_TIDY_RECALL_THRESHOLD");
|
||||
same(threshold?.value, "0.8", "比例值原样显示(后端已归一化)");
|
||||
|
||||
// ── ② 「已改」只由 isDefault 决定(不反推 .env) ─────────────────────────────
|
||||
const changed: AdminConfigFacts = {
|
||||
...DEFAULTS,
|
||||
facts: DEFAULTS.facts.map((f) =>
|
||||
f.key === "MEMORY_TIDY_INTERVAL_SEC" ? { ...f, value: "3600", isDefault: false } : f,
|
||||
),
|
||||
};
|
||||
const changedRow = memoryTidyRows(changed).find((r) => r.key === "MEMORY_TIDY_INTERVAL_SEC");
|
||||
same(changedRow?.value, "3600 秒", "改过的值如实显示");
|
||||
same(changedRow?.state, "已改", "isDefault=false ⇒ 显示「已改」");
|
||||
same(
|
||||
memoryTidyRows(changed).filter((r) => r.state === "已改").length,
|
||||
1,
|
||||
"只有真的改过的那一行是「已改」",
|
||||
);
|
||||
|
||||
// ── ③ 后端没给的键如实显示「未报告」,绝不编值 ──────────────────────────────
|
||||
const partial: AdminConfigFacts = {
|
||||
facts: DEFAULTS.facts.filter((f) => f.key !== "MEMORY_TIDY_SCAN_LIMIT"),
|
||||
quietHours: DEFAULTS.quietHours,
|
||||
};
|
||||
const missing = memoryTidyRows(partial).find((r) => r.key === "MEMORY_TIDY_SCAN_LIMIT");
|
||||
same(missing?.value, FACT_MISSING_VALUE, "缺的键显示「未报告」而不是 0 或默认值");
|
||||
same(missing?.state, FACT_MISSING_VALUE, "缺的键状态也是「未报告」");
|
||||
same(memoryTidyRows(null).length, 7, "整份响应都没拿到时,7 行仍然都在(全为未报告)");
|
||||
ok(
|
||||
memoryTidyRows(null).every((r) => r.state === FACT_MISSING_VALUE),
|
||||
"没有响应时不留空白行,全部如实标「未报告」",
|
||||
);
|
||||
|
||||
// ── ④ 安静时段:窗口摘要 + 用途文案(新语义,不得说「夜间全静默」) ─────────
|
||||
same(quietHoursRows(DEFAULTS).map((r) => r.key), [...QUIET_HOURS_KEYS], "安静时段两行");
|
||||
same(quietHoursSummary(DEFAULTS.quietHours), "23:00-07:00(跨午夜)", "跨午夜窗口如实标注");
|
||||
same(
|
||||
quietHoursSummary({ startHour: 1, endHour: 5, disabled: false, label: "01:00-05:00" }),
|
||||
"01:00-05:00",
|
||||
"非跨午夜窗口不加多余后缀",
|
||||
);
|
||||
same(
|
||||
quietHoursSummary({ startHour: 23, endHour: 23, disabled: true, label: "已关闭" }),
|
||||
"已关闭(安静时段从不生效)",
|
||||
"窗口关闭必须说「从不生效」,不能画成 23:00-23:00 的假窗口",
|
||||
);
|
||||
same(
|
||||
quietHoursSummary({ startHour: 23, endHour: 7, disabled: false, label: "23:00-07:00", fallbackToDefault: true }),
|
||||
"23:00-07:00(两端写 0 = 未填,运行时回落到默认窗口)",
|
||||
"0/0 被当成「未填」时必须说明回落,不能谎报成「已关闭」",
|
||||
);
|
||||
same(quietHoursSummary(null), FACT_MISSING_VALUE, "没有 quietHours 时如实报未报告");
|
||||
|
||||
const purpose = quietHoursPurpose();
|
||||
ok(has(purpose, "定时主动思考"), "用途文案必须点明它服务「定时主动思考 → 主动搭话」");
|
||||
ok(has(purpose, "不静默回复") || has(purpose, "不会因为到了安静时段就被静默"), "用途文案必须说清回复侧不被静默");
|
||||
ok(!has(purpose, "夜间全静默"), "不得沿用已废弃的旧语义「夜间全静默」");
|
||||
ok(!has(purpose, "群聊静默"), "群聊夜间静默已被取消,不得写成有");
|
||||
|
||||
// ── ⑤ factRows 对未知键不崩、原样透出键名 ───────────────────────────────────
|
||||
const unknown = factRows(DEFAULTS, ["NOT_A_REAL_KEY"]);
|
||||
same(unknown[0].label, "NOT_A_REAL_KEY", "没有元数据的键用键名兜底(不编中文标签)");
|
||||
same(unknown[0].value, FACT_MISSING_VALUE, "没有元数据的键如实「未报告」");
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`configFacts: ${checks} checks passed`);
|
||||
Reference in new issue
Block a user