@modusensus/dsh-mneme 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -19
- package/docs/CONFIGURATION.md +43 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +581 -29
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3537 -1205
- package/lib/config.js +139 -1
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +92 -2
- package/lib/inject.js +189 -18
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +87 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +193 -66
- package/lib/settings.js +112 -14
- package/lib/store.js +347 -21
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +116 -2
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +581 -29
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +139 -1
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +92 -2
- package/src/inject.js +189 -18
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +87 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +193 -66
- package/src/settings.js +112 -14
- package/src/store.js +347 -21
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +116 -2
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +375 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +779 -154
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +7 -7
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +355 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +7 -0
- package/test/wiki-t1-jobs.test.js +125 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Issue #622 可观测性回归:错峰时段串解析失败不许静默。
|
|
2
|
+
//
|
|
3
|
+
// parsePeakSpec 的失败语义(整串视为未配置)是刻意的契约——排程是省钱手段,绝不
|
|
4
|
+
// 该因为写错格式把蒸馏/巩固停掉。但「不救错」不等于「不吭声」:#424 的排查只能靠
|
|
5
|
+
// 「高峰期还在巩固」倒推,正是静默的全貌。本文件锁三件事:
|
|
6
|
+
// ① invalidPeakSpecs 只报「非空且解析失败」——空串/未配置/合法串/全角归一救得了
|
|
7
|
+
// 的写法一律不报(验收第 2 条:不骚扰没配错峰的用户;#621 已落归一,两处共存);
|
|
8
|
+
// ② index.js 的启动告警真的接在 cfg 装配之后(接线锁,仿 failure-retention 的
|
|
9
|
+
// 写法):告警读的是装配后的 cfg——面板改的 flatFlags 合并进来的值也算数;
|
|
10
|
+
// ③ /features 答复的 invalidPeakHours 与 helper 同源(api.test.js 里另有往返)。
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { parsePeakSpec, invalidPeakSpecs, PEAK_SPEC_KEYS } from "../src/peak-hours.js";
|
|
17
|
+
|
|
18
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const indexSrc = readFileSync(path.join(here, "..", "src", "index.js"), "utf8");
|
|
20
|
+
|
|
21
|
+
test("invalidPeakSpecs: 只报非空且解析失败的键;空串/未配置/合法串不报", () => {
|
|
22
|
+
// 未配置(undefined)与空串:合法的「未配置」,不告警
|
|
23
|
+
assert.deepEqual(invalidPeakSpecs({}), {});
|
|
24
|
+
assert.deepEqual(invalidPeakSpecs({ summarizePeakHours: "" }), {});
|
|
25
|
+
assert.deepEqual(invalidPeakSpecs({ summarizePeakHours: " " }), {});
|
|
26
|
+
assert.deepEqual(invalidPeakSpecs({ dreamPeakHours: undefined }), {});
|
|
27
|
+
// 合法串:不报
|
|
28
|
+
assert.deepEqual(invalidPeakSpecs({ summarizePeakHours: "09:00-18:00" }), {});
|
|
29
|
+
assert.deepEqual(invalidPeakSpecs({ dreamPeakHours: "mon-fri 08:00-12:00,14:00-18:00" }), {});
|
|
30
|
+
// 全角等价写法:本批原基于未含 #621 归一的主线,当时 parsePeakSpec 解析失败、
|
|
31
|
+
// 「报」是真话。**#621 已并入 main**(全角逗号/冒号/数字归一),解析现在成功,
|
|
32
|
+
// 本断言随之翻转为不报——这正是 helper「只转述解析器、不自带第二套对错标准」
|
|
33
|
+
// 的意义:判据翻转由解析器驱动,本文件不需要改任何逻辑。
|
|
34
|
+
// ⚠️ 原注释里「两批共存无需联动改动」这句是错的:判据确实会翻,只是翻的位置
|
|
35
|
+
// 在这里而不在 src/。合并 #621 后必须同步翻这一行,否则就是假红。
|
|
36
|
+
assert.deepEqual(invalidPeakSpecs({ summarizePeakHours: "09:00-18:00" }), {});
|
|
37
|
+
// 非法串:报,且带原串(告警要对得上配置文件里那一行)
|
|
38
|
+
assert.deepEqual(invalidPeakSpecs({ summarizePeakHours: "24:00-08:00" }), { summarizePeakHours: "24:00-08:00" });
|
|
39
|
+
assert.deepEqual(invalidPeakSpecs({ dreamPeakHours: "09.00-18.00" }), { dreamPeakHours: "09.00-18.00" });
|
|
40
|
+
// 两个键互相独立,各查各的("ok" 没有冒号、没有时段段——解析器视为非法,报得对)
|
|
41
|
+
const both = invalidPeakSpecs({ summarizePeakHours: "09:00-18:00", dreamPeakHours: "24:00-01:00" });
|
|
42
|
+
assert.deepEqual(both, { dreamPeakHours: "24:00-01:00" });
|
|
43
|
+
// 非对象(裸调用方传 undefined 之类)不抛、返回空
|
|
44
|
+
assert.deepEqual(invalidPeakSpecs(undefined), {});
|
|
45
|
+
assert.deepEqual(invalidPeakSpecs(null), {});
|
|
46
|
+
// 非字符串值(垃圾输入)不抛、不报——typeof 守卫
|
|
47
|
+
assert.deepEqual(invalidPeakSpecs({ summarizePeakHours: 42 }), {});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("PEAK_SPEC_KEYS 恰好覆盖两个时段键,parsePeakSpec 对它们的失败语义不变", () => {
|
|
51
|
+
// 键名单独成表:告警点与提示点都要认同一份,谁加键谁改表(不各写数组各自漂移)
|
|
52
|
+
assert.deepEqual(PEAK_SPEC_KEYS, ["summarizePeakHours", "dreamPeakHours"]);
|
|
53
|
+
// 本 issue 不动解析语义:非法 → null、空 → null、合法 → 结构(#239 契约原样)
|
|
54
|
+
assert.equal(parsePeakSpec("24:00-08:00"), null);
|
|
55
|
+
assert.equal(parsePeakSpec(""), null);
|
|
56
|
+
assert.ok(parsePeakSpec("09:00-18:00"));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("接线锁:index.js 的启动告警读装配后的 cfg,且在 sessionGate 之前", () => {
|
|
60
|
+
const cfgAt = indexSrc.indexOf("const cfg = { ...applyLightModePreset({ ...rawCfg, lightMode }), ...flatFlags };");
|
|
61
|
+
const callAt = indexSrc.indexOf("invalidPeakSpecs(cfg)");
|
|
62
|
+
const gateAt = indexSrc.indexOf("const sessionGate = createSessionGate(cfg);");
|
|
63
|
+
assert.ok(cfgAt > 0, "装配行必须在");
|
|
64
|
+
assert.ok(callAt > cfgAt, "告警必须排在 cfg 装配之后——读 rawCfg 会漏掉面板改的 flatFlags");
|
|
65
|
+
assert.ok(gateAt > callAt, "告警在 sessionGate 之前(纯 log,不参与装配顺序的实质,锁在这里只为防它被挪进更早、读不到合并值的分支)");
|
|
66
|
+
// 只 log:告警分支里不许出现改写 cfg 的语句(行为零变化是本 issue 的前提)
|
|
67
|
+
const block = indexSrc.slice(callAt, gateAt);
|
|
68
|
+
assert.ok(!/cfg\s*=[^=]/.test(block), "告警分支不得改写 cfg");
|
|
69
|
+
assert.ok(block.includes("logger?.warn?.("), "失败可见性走 logger.warn");
|
|
70
|
+
});
|
package/test/persona-api.test.js
CHANGED
|
@@ -75,7 +75,7 @@ test("internal PUT /persona without token is rejected and store untouched", asyn
|
|
|
75
75
|
const res = new FakeRes();
|
|
76
76
|
await route.handler(req("/api/dsh-mneme/persona", "PUT", { persona: { name: "evil" } }), res);
|
|
77
77
|
assert.equal(res.statusCode, 401);
|
|
78
|
-
assert.deepEqual(settings.getPersona(), { default: {}, byAgent: {} });
|
|
78
|
+
assert.deepEqual(settings.getPersona(), { default: {}, byAgent: {}, bySession: {} });
|
|
79
79
|
store.close();
|
|
80
80
|
});
|
|
81
81
|
|
|
@@ -140,7 +140,7 @@ test("standalone GET/PUT /persona behind Bearer gate round-trips", async () => {
|
|
|
140
140
|
try {
|
|
141
141
|
const get1 = await fetch(`${t.base}/persona`, { headers: t.auth });
|
|
142
142
|
assert.equal(get1.status, 200);
|
|
143
|
-
assert.deepEqual((await get1.json()).persona, { default: {}, byAgent: {} });
|
|
143
|
+
assert.deepEqual((await get1.json()).persona, { default: {}, byAgent: {}, bySession: {} });
|
|
144
144
|
const put = await fetch(`${t.base}/persona`, {
|
|
145
145
|
method: "PUT",
|
|
146
146
|
headers: { ...t.auth, "content-type": "application/json" },
|
|
@@ -165,7 +165,7 @@ test("standalone /persona without Bearer is 401", async () => {
|
|
|
165
165
|
try {
|
|
166
166
|
const res = await fetch(`${t.base}/persona`, { method: "PUT", body: "{}" });
|
|
167
167
|
assert.equal(res.status, 401);
|
|
168
|
-
assert.deepEqual(t.settings.getPersona(), { default: {}, byAgent: {} });
|
|
168
|
+
assert.deepEqual(t.settings.getPersona(), { default: {}, byAgent: {}, bySession: {} });
|
|
169
169
|
} finally {
|
|
170
170
|
t.close();
|
|
171
171
|
}
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
// 绑会话的人格卡(#606)。为什么需要这一维:人格卡原本只有 agentPreset 一个寻址键,
|
|
2
|
+
// 而 DS Bot 这类宿主把它写成常量——实测 bot 工作区 50 个会话里 43 个是 "standard",
|
|
3
|
+
// 四条主 bot(Amarre / Ephor / Loom / Persis)全部相同,但它们的 session.id 各不相同。
|
|
4
|
+
// 所以「一个 bot 一张卡」只能用会话 id 当键。
|
|
5
|
+
//
|
|
6
|
+
// 本文件锁四件事:
|
|
7
|
+
// 1. 纯加法:没有 bySession 的老数据读回仍是空对象,注入文本逐字节不变。
|
|
8
|
+
// 2. 优先级:bySession > byAgent > default(会话比预设具体)。
|
|
9
|
+
// 3. 严格寻址的两维合判:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底。
|
|
10
|
+
// 4. scenario / mesExample:未设置零输出,设置后排在 replyStyle 之后。
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
14
|
+
import { createStore } from "../src/store.js";
|
|
15
|
+
import { createService } from "../src/service.js";
|
|
16
|
+
import { createInjector } from "../src/inject.js";
|
|
17
|
+
import { createApi } from "../src/api.js";
|
|
18
|
+
import { createSettings } from "../src/settings.js";
|
|
19
|
+
import { resolvePersonaCard, renderUserSettingsBlock } from "../src/user-settings.js";
|
|
20
|
+
|
|
21
|
+
class FakeRes extends EventEmitter {
|
|
22
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
23
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
24
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function req(path, method = "GET") {
|
|
28
|
+
return { url: path, method, headers: {}, on() {}, emit() {} };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 注入器 + API 共用同一份 settings/config(同源纪律),两边都能按 {agentPreset, sessionId} 寻址。
|
|
32
|
+
function setup(config = {}) {
|
|
33
|
+
const store = createStore(":memory:");
|
|
34
|
+
const service = createService({ store, mirror: null, config });
|
|
35
|
+
const settings = createSettings(store.db);
|
|
36
|
+
const contexts = [];
|
|
37
|
+
const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
|
|
38
|
+
createInjector(ctx, service, settings, config);
|
|
39
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
40
|
+
// 造 ctx:只给要测的那几个键,缺省不给(模拟宿主解析不到)。
|
|
41
|
+
const makeCtx = ({ agentPreset = null, sessionId = null, parentSession = null } = {}) => {
|
|
42
|
+
if (!agentPreset && !sessionId && !parentSession) return {};
|
|
43
|
+
const session = {};
|
|
44
|
+
if (sessionId) session.id = sessionId;
|
|
45
|
+
if (agentPreset || parentSession) {
|
|
46
|
+
session.header = {};
|
|
47
|
+
if (agentPreset) session.header.agentPreset = agentPreset;
|
|
48
|
+
if (parentSession) session.header.parentSession = parentSession;
|
|
49
|
+
}
|
|
50
|
+
return { agent: { session } };
|
|
51
|
+
};
|
|
52
|
+
const injectorText = (opts) => settingsCtx.text(makeCtx(opts));
|
|
53
|
+
const routes = [];
|
|
54
|
+
createApi(
|
|
55
|
+
{ webServer: { register: (route) => { routes.push(route); return () => {}; } } },
|
|
56
|
+
service,
|
|
57
|
+
settings,
|
|
58
|
+
{ add: () => {}, remove: () => {}, list: () => [] },
|
|
59
|
+
undefined,
|
|
60
|
+
undefined,
|
|
61
|
+
"",
|
|
62
|
+
config
|
|
63
|
+
);
|
|
64
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/persona");
|
|
65
|
+
const preview = async ({ agent = null, session = null, parent = null } = {}) => {
|
|
66
|
+
const qs = [];
|
|
67
|
+
if (agent) qs.push(`agent=${encodeURIComponent(agent)}`);
|
|
68
|
+
if (session) qs.push(`session=${encodeURIComponent(session)}`);
|
|
69
|
+
if (parent) qs.push(`parent=${encodeURIComponent(parent)}`);
|
|
70
|
+
const res = new FakeRes();
|
|
71
|
+
await route.handler(req(`/api/dsh-mneme/persona${qs.length ? `?${qs.join("&")}` : ""}`), res);
|
|
72
|
+
return JSON.parse(res.body);
|
|
73
|
+
};
|
|
74
|
+
return { store, settings, settingsCtx, injectorText, preview };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function seed(settings) {
|
|
78
|
+
settings.setProfile("用户是规划背景");
|
|
79
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true }); // default
|
|
80
|
+
settings.setPersonaFor("standard", { name: "标准卡", personality: "通用", enabled: true }); // byAgent
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- 1. 纯加法:老数据零变化 -------------------------------------------------
|
|
84
|
+
|
|
85
|
+
test("老数据(只有 {default, byAgent})读回 bySession 为空对象,注入文本逐字节不变", () => {
|
|
86
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
87
|
+
// 直写 v1.2 形(无 bySession 键)模拟存量库
|
|
88
|
+
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
89
|
+
.run(JSON.stringify({
|
|
90
|
+
default: { name: "Amarre", enabled: true },
|
|
91
|
+
byAgent: { standard: { name: "标准卡", enabled: true } }
|
|
92
|
+
}));
|
|
93
|
+
const got = settings.getPersona();
|
|
94
|
+
assert.deepEqual(got.bySession, {}, "缺 bySession 键必须读成空对象,不是 undefined");
|
|
95
|
+
assert.equal(got.default.name, "Amarre");
|
|
96
|
+
assert.equal(got.byAgent.standard.name, "标准卡");
|
|
97
|
+
// 寻址结果与改动前一致:按 preset 命中 byAgent,不因多了这一维而变
|
|
98
|
+
assert.equal(resolvePersonaCard(settings, "standard").source, "byAgent");
|
|
99
|
+
assert.equal(resolvePersonaCard(settings, "unknown").source, "default");
|
|
100
|
+
store.close();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("v1 单对象老数据:读作 default,bySession 为 {}(零迁移)", () => {
|
|
104
|
+
const { store, settings } = setup();
|
|
105
|
+
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
106
|
+
.run(JSON.stringify({ name: "Amarre", personality: "轻松幽默", enabled: true }));
|
|
107
|
+
const got = settings.getPersona();
|
|
108
|
+
assert.equal(got.default.name, "Amarre");
|
|
109
|
+
assert.deepEqual(got.byAgent, {});
|
|
110
|
+
assert.deepEqual(got.bySession, {});
|
|
111
|
+
store.close();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("没设 scenario / mesExample 时,注入文本与历史版本逐字节一致", () => {
|
|
115
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
116
|
+
settings.setProfile("用户是规划背景");
|
|
117
|
+
settings.setRules(["不提及用户名"]);
|
|
118
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", replyStyle: "先结论后展开", enabled: true });
|
|
119
|
+
const text = injectorText({});
|
|
120
|
+
assert.equal(text, [
|
|
121
|
+
"[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
122
|
+
"- 你的人格名字:Amarre",
|
|
123
|
+
"- 人格性格设定:轻松幽默",
|
|
124
|
+
"- 回复要求:先结论后展开",
|
|
125
|
+
"- 用户画像:用户是规划背景",
|
|
126
|
+
"- 规则:不提及用户名"
|
|
127
|
+
].join("\n"), "两个新字段都没设时不许留下空标签或空行");
|
|
128
|
+
store.close();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---- 2. 存储往返 -------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
test("bySession 存取往返:写入 → 读回 → 列出 → 删除", () => {
|
|
134
|
+
const { store, settings } = setup();
|
|
135
|
+
const card = { name: "Ephor", personality: "审计口径", enabled: true };
|
|
136
|
+
assert.equal(settings.setPersonaForSession("session-389faaa7", card), true);
|
|
137
|
+
assert.deepEqual(resolvePersonaCard(settings, null, { sessionId: "session-389faaa7" }).card, card);
|
|
138
|
+
assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
|
|
139
|
+
// 与 byAgent 互不干扰
|
|
140
|
+
settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
|
|
141
|
+
assert.deepEqual(settings.listPersonas(), ["standard"]);
|
|
142
|
+
assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
|
|
143
|
+
// 删
|
|
144
|
+
assert.equal(settings.removePersonaForSession("session-389faaa7"), true);
|
|
145
|
+
assert.deepEqual(settings.listPersonaSessions(), []);
|
|
146
|
+
assert.equal(settings.removePersonaForSession("session-389faaa7"), false, "删不存在的卡返回 false");
|
|
147
|
+
store.close();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("setPersonaForSession 拒绝空 id 与 default(兜底位不可被顶替)", () => {
|
|
151
|
+
const { store, settings } = setup();
|
|
152
|
+
assert.equal(settings.setPersonaForSession("", { name: "x" }), false);
|
|
153
|
+
assert.equal(settings.setPersonaForSession(" ", { name: "x" }), false);
|
|
154
|
+
assert.equal(settings.setPersonaForSession(null, { name: "x" }), false);
|
|
155
|
+
assert.equal(settings.setPersonaForSession("default", { name: "x" }), false);
|
|
156
|
+
assert.deepEqual(settings.listPersonaSessions(), []);
|
|
157
|
+
store.close();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("写空卡等于删除该会话卡(与 setPersonaFor 同形)", () => {
|
|
161
|
+
const { store, settings } = setup();
|
|
162
|
+
settings.setPersonaForSession("s1", { name: "甲", enabled: true });
|
|
163
|
+
assert.deepEqual(settings.listPersonaSessions(), ["s1"]);
|
|
164
|
+
settings.setPersonaForSession("s1", {});
|
|
165
|
+
assert.deepEqual(settings.listPersonaSessions(), [], "空卡不留空壳键");
|
|
166
|
+
store.close();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("setPersona 单对象形只改 default,bySession 保留", () => {
|
|
170
|
+
const { store, settings } = setup();
|
|
171
|
+
settings.setPersonaForSession("s1", { name: "会话卡", enabled: true });
|
|
172
|
+
settings.setPersona({ name: "Amarre-新", enabled: true }); // 旧面板写法
|
|
173
|
+
const got = settings.getPersona();
|
|
174
|
+
assert.equal(got.default.name, "Amarre-新");
|
|
175
|
+
assert.equal(got.bySession.s1.name, "会话卡", "bySession 必须活过一次 legacy 形写入");
|
|
176
|
+
store.close();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("多卡形全量替换:带 bySession 键即整份替换", () => {
|
|
180
|
+
const { store, settings } = setup();
|
|
181
|
+
settings.setPersonaForSession("old", { name: "旧卡", enabled: true });
|
|
182
|
+
settings.setPersona({ default: { name: "新默认", enabled: true }, byAgent: {}, bySession: { new: { name: "新卡", enabled: true } } });
|
|
183
|
+
assert.deepEqual(settings.listPersonaSessions(), ["new"]);
|
|
184
|
+
store.close();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---- 3. 寻址优先级 -----------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
test("优先级:bySession 盖过 byAgent(会话比预设具体)", () => {
|
|
190
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
191
|
+
seed(settings); // default=Amarre, byAgent.standard=标准卡
|
|
192
|
+
settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这次会话", enabled: true });
|
|
193
|
+
const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
194
|
+
assert.ok(text.includes("你的人格名字:会话专属"), "会话卡必须赢");
|
|
195
|
+
assert.ok(!text.includes("标准卡"), "byAgent 卡不该同时出现");
|
|
196
|
+
assert.ok(!text.includes("Amarre"), "default 更不该出现");
|
|
197
|
+
store.close();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("优先级:会话卡被关掉时落回 byAgent(不是直接跳 default)", () => {
|
|
201
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
202
|
+
seed(settings);
|
|
203
|
+
settings.setPersonaForSession("session-abc", { name: "会话专属", enabled: false });
|
|
204
|
+
const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
205
|
+
assert.ok(text.includes("你的人格名字:标准卡"), "关掉的会话卡让位给身份卡");
|
|
206
|
+
assert.ok(!text.includes("会话专属"));
|
|
207
|
+
store.close();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("优先级:没有会话卡时按 byAgent 走(老行为不变)", () => {
|
|
211
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
212
|
+
seed(settings);
|
|
213
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-unknown" });
|
|
214
|
+
assert.equal(hit.source, "byAgent");
|
|
215
|
+
assert.equal(hit.card.name, "标准卡");
|
|
216
|
+
store.close();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("两个键都没有:落 default", () => {
|
|
220
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
221
|
+
seed(settings);
|
|
222
|
+
const hit = resolvePersonaCard(settings, null, { sessionId: null });
|
|
223
|
+
assert.equal(hit.source, "default");
|
|
224
|
+
assert.equal(hit.card.name, "Amarre");
|
|
225
|
+
store.close();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("只给 sessionId、不给 agentId 也能命中会话卡(宿主可能只有会话 id)", () => {
|
|
229
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
230
|
+
seed(settings);
|
|
231
|
+
settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
|
|
232
|
+
const text = injectorText({ sessionId: "session-only" });
|
|
233
|
+
assert.ok(text.includes("你的人格名字:仅会话卡"));
|
|
234
|
+
store.close();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ---- 4. 严格寻址:两维合判 ---------------------------------------------------
|
|
238
|
+
|
|
239
|
+
test("strict:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底", () => {
|
|
240
|
+
// #587 的例外是「一张卡都没建」才回落 default。用户建了会话卡就已经做过分区,
|
|
241
|
+
// 此时一个没建卡的 preset 会话再拿 default 去兜,等于把分区抹掉一半。
|
|
242
|
+
const { store, settings } = setup();
|
|
243
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
244
|
+
assert.deepEqual(settings.getPersona().byAgent, {}, "前提:byAgent 为空");
|
|
245
|
+
assert.deepEqual(settings.getPersona().bySession, {}, "前提:bySession 也为空");
|
|
246
|
+
// 两维都空 → 仍回落 default(#587 例外保持)
|
|
247
|
+
assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "default");
|
|
248
|
+
// 只建会话卡之后,例外立即失效
|
|
249
|
+
settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
|
|
250
|
+
assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "none");
|
|
251
|
+
// 而那张会话卡自己仍然命中
|
|
252
|
+
assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-x" }).source, "bySession");
|
|
253
|
+
store.close();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("strict:只建了 byAgent 卡时,按会话寻址的会话也不许被兜底", () => {
|
|
257
|
+
const { store, settings } = setup();
|
|
258
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
259
|
+
settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
|
|
260
|
+
// 有会话 id 但没建会话卡 → none(用户已经做过 byAgent 分区)
|
|
261
|
+
assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-y" }).source, "none");
|
|
262
|
+
store.close();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("strict:两个键都解析不到时仍用 default(无名会话是 default 的受众)", () => {
|
|
266
|
+
const { store, settings } = setup();
|
|
267
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
268
|
+
settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
|
|
269
|
+
assert.equal(resolvePersonaCard(settings, null, { strict: true }).source, "default");
|
|
270
|
+
store.close();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---- 5. scenario / mesExample ------------------------------------------------
|
|
274
|
+
|
|
275
|
+
test("scenario / mesExample 设置在 replyStyle 之后渲染", () => {
|
|
276
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
277
|
+
settings.setPersona({
|
|
278
|
+
name: "Amarre",
|
|
279
|
+
personality: "轻松幽默",
|
|
280
|
+
replyStyle: "先结论后展开",
|
|
281
|
+
scenario: "深夜陪你赶论文",
|
|
282
|
+
mesExample: "桉桉:好累。Amarre:先睡,剩下的我盯着。",
|
|
283
|
+
enabled: true
|
|
284
|
+
});
|
|
285
|
+
const text = injectorText({});
|
|
286
|
+
const idxReply = text.indexOf("回复要求:先结论后展开");
|
|
287
|
+
const idxScene = text.indexOf("场景设定:深夜陪你赶论文");
|
|
288
|
+
const idxExample = text.indexOf("对话示例:桉桉:好累。");
|
|
289
|
+
assert.ok(idxReply > -1 && idxScene > -1 && idxExample > -1, "三段都在");
|
|
290
|
+
assert.ok(idxScene > idxReply, "场景在语气之后:身份 → 语气 → 场景 → 示例");
|
|
291
|
+
assert.ok(idxExample > idxScene, "示例在场景之后");
|
|
292
|
+
store.close();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("scenario 只设一个字段时只出一行,不牵连另一个", () => {
|
|
296
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
297
|
+
settings.setPersona({ name: "Amarre", scenario: "只在夜里出现", enabled: true });
|
|
298
|
+
const text = injectorText({});
|
|
299
|
+
assert.ok(text.includes("场景设定:只在夜里出现"));
|
|
300
|
+
assert.ok(!text.includes("对话示例"), "没设 mesExample 就不出那一行");
|
|
301
|
+
store.close();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("卡里存着空白 scenario / mesExample 时不出空标签(未设置零输出)", () => {
|
|
305
|
+
// 未设置的字段被 parsePersona 白名单挡在外面,但「存了空白」是另一条路:
|
|
306
|
+
// 卡里确实有这个键、值是空白串。这一行必须整行不出现,不能渲染成「场景设定:」。
|
|
307
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
308
|
+
settings.setPersona({ name: "Amarre", scenario: " ", mesExample: "\t ", enabled: true });
|
|
309
|
+
const card = settings.getPersona().default;
|
|
310
|
+
assert.equal(card.scenario, " ", "空白串仍被存下(白名单不负责裁空白)");
|
|
311
|
+
const text = injectorText({});
|
|
312
|
+
assert.ok(!text.includes("场景设定"), "空白 scenario 不许渲染出标签");
|
|
313
|
+
assert.ok(!text.includes("对话示例"), "空白 mesExample 不许渲染出标签");
|
|
314
|
+
assert.ok(text.includes("你的人格名字:Amarre"), "其余段落照常");
|
|
315
|
+
store.close();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("scenario / mesExample 走同一个转义(花括号不许漏出去)", () => {
|
|
319
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
320
|
+
settings.setPersona({ name: "Amarre", scenario: "{{evil}}", mesExample: "{{x}}", enabled: true });
|
|
321
|
+
const text = injectorText({});
|
|
322
|
+
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
323
|
+
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
324
|
+
store.close();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("scenario / mesExample 只在卡 enabled 时渲染(关掉的卡一个字都不出)", () => {
|
|
328
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
329
|
+
settings.setPersona({ name: "Amarre", scenario: "深夜", enabled: false });
|
|
330
|
+
settings.setProfile("有画像");
|
|
331
|
+
const text = injectorText({});
|
|
332
|
+
assert.ok(!text.includes("场景设定"), "卡关着时新字段同样不渲染");
|
|
333
|
+
assert.ok(text.includes("用户画像:"));
|
|
334
|
+
store.close();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("scenario / mesExample 过字段白名单(未知字段仍被丢弃)", () => {
|
|
338
|
+
const { store, settings } = setup();
|
|
339
|
+
settings.setPersona({ name: "Amarre", scenario: "场景", mesExample: "示例", sneaky: "x" });
|
|
340
|
+
const card = settings.getPersona().default;
|
|
341
|
+
assert.equal(card.scenario, "场景");
|
|
342
|
+
assert.equal(card.mesExample, "示例");
|
|
343
|
+
assert.equal("sneaky" in card, false, "白名单没放宽");
|
|
344
|
+
store.close();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ---- 6. 端到端:注入链路 + 预览路由 ------------------------------------------
|
|
348
|
+
|
|
349
|
+
test("注入链路端到端:同一个 agentPreset、不同 sessionId 拿到不同人格", () => {
|
|
350
|
+
// 这就是 #606 要解决的场景本身:DS Bot 里四个 bot 的 agentPreset 全是 standard。
|
|
351
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
352
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
353
|
+
settings.setPersonaForSession("session-d530e132", { name: "Amarre", personality: "主控", enabled: true });
|
|
354
|
+
settings.setPersonaForSession("session-389faaa7", { name: "Ephor", personality: "审计", enabled: true });
|
|
355
|
+
const a = injectorText({ agentPreset: "standard", sessionId: "session-d530e132" });
|
|
356
|
+
const b = injectorText({ agentPreset: "standard", sessionId: "session-389faaa7" });
|
|
357
|
+
assert.ok(a.includes("你的人格名字:Amarre"));
|
|
358
|
+
assert.ok(b.includes("你的人格名字:Ephor"), "同一个 standard 预设下,会话卡必须分得开");
|
|
359
|
+
assert.ok(!b.includes("你的人格名字:Amarre"));
|
|
360
|
+
store.close();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("预览路由支持 &session=,且与注入器逐字节同源", async () => {
|
|
364
|
+
const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
|
|
365
|
+
seed(settings);
|
|
366
|
+
settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这里", enabled: true });
|
|
367
|
+
const fromInjector = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
368
|
+
const body = await preview({ agent: "standard", session: "session-abc" });
|
|
369
|
+
assert.equal(body.preview.text, fromInjector, "预览必须等于注入器这一帧的输出");
|
|
370
|
+
assert.equal(body.preview.source, "bySession");
|
|
371
|
+
assert.equal(body.preview.session, "session-abc", "面板要靠它说清用的是哪张会话卡");
|
|
372
|
+
assert.ok(body.preview.text.includes("你的人格名字:会话专属"));
|
|
373
|
+
store.close();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("预览只给 session 不给 agent 也走会话卡", async () => {
|
|
377
|
+
const { store, settings, preview } = setup({ personaEnabled: true });
|
|
378
|
+
seed(settings);
|
|
379
|
+
settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
|
|
380
|
+
const body = await preview({ session: "session-only" });
|
|
381
|
+
assert.equal(body.preview.source, "bySession");
|
|
382
|
+
assert.equal(body.preview.agent, null);
|
|
383
|
+
assert.equal(body.preview.session, "session-only");
|
|
384
|
+
store.close();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("预览的 session 参数在 strict 下同样生效(不许比注入更乐观)", async () => {
|
|
388
|
+
const { store, settings, preview } = setup({ personaEnabled: true, personaStrictAddressing: true });
|
|
389
|
+
seed(settings);
|
|
390
|
+
const miss = await preview({ session: "session-unknown" });
|
|
391
|
+
assert.equal(miss.preview.source, "none", "建过卡、这次会话没建 → 不兜底");
|
|
392
|
+
assert.ok(!miss.preview.text.includes("Amarre"));
|
|
393
|
+
settings.setPersonaForSession("session-hit", { name: "命中卡", enabled: true });
|
|
394
|
+
const hit = await preview({ session: "session-hit" });
|
|
395
|
+
assert.equal(hit.preview.source, "bySession");
|
|
396
|
+
store.close();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ---- 7. 健壮性 ---------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
test("session.id 取不到(宿主 accessor 抛错)时不影响 agentPreset 寻址", () => {
|
|
402
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
403
|
+
seed(settings);
|
|
404
|
+
// session.id 是抛错的 getter,header 正常——两个键各自护住,不许互相拖累
|
|
405
|
+
const hostile = {
|
|
406
|
+
agent: {
|
|
407
|
+
session: {
|
|
408
|
+
get id() { throw new Error("no id accessor"); },
|
|
409
|
+
header: { agentPreset: "standard" }
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const text = settingsCtx.text(hostile);
|
|
414
|
+
assert.ok(text.includes("你的人格名字:标准卡"), "id 抛错不该让 preset 也丢掉");
|
|
415
|
+
store.close();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("bySession 里的损坏卡(非对象)被丢弃,不抛错", () => {
|
|
419
|
+
const { store, settings } = setup();
|
|
420
|
+
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
421
|
+
.run(JSON.stringify({ default: {}, byAgent: {}, bySession: { bad: "not a card", good: { name: "好卡", enabled: true } } }));
|
|
422
|
+
const got = settings.getPersona();
|
|
423
|
+
assert.equal("bad" in got.bySession, false, "坏卡读成空对象后不收(与 byAgent 同口径)");
|
|
424
|
+
assert.equal(got.bySession.good.name, "好卡");
|
|
425
|
+
store.close();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("renderUserSettingsBlock 返回值带 session 字段(面板定位用)", () => {
|
|
429
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
430
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
431
|
+
const out = renderUserSettingsBlock({ settings, config: { personaEnabled: true }, agentId: "standard", sessionId: "s1" });
|
|
432
|
+
assert.equal(out.session, "s1");
|
|
433
|
+
assert.equal(out.agent, "standard");
|
|
434
|
+
assert.equal(out.source, "default");
|
|
435
|
+
store.close();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ---- 8. 子 agent 继承父会话人格卡 --------------------------------------------
|
|
439
|
+
//
|
|
440
|
+
// 子 agent 是新会话(header 带 parentSession / origin:"subagent"),用户不可能给每个
|
|
441
|
+
// 派生会话建卡。strict 一开,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从
|
|
442
|
+
// 「自称父会话的人格」变成「不知道自己是谁」。所以自己没卡时继承父会话的卡。
|
|
443
|
+
|
|
444
|
+
test("子 agent 没自己的卡时继承父会话卡(source 是 parentSession)", () => {
|
|
445
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
446
|
+
settings.setPersona({ name: "兜底卡", enabled: true });
|
|
447
|
+
settings.setPersonaForSession("session-parent", { name: "主控人格", personality: "轻松诙谐", enabled: true });
|
|
448
|
+
const text = injectorText({ agentPreset: "standard", sessionId: "session-child", parentSession: "session-parent" });
|
|
449
|
+
assert.ok(text.includes("你的人格名字:主控人格"), "子 agent 必须拿到父会话那张卡");
|
|
450
|
+
assert.ok(!text.includes("兜底卡"), "不许回落到 default");
|
|
451
|
+
const out = renderUserSettingsBlock({
|
|
452
|
+
settings,
|
|
453
|
+
config: { personaEnabled: true },
|
|
454
|
+
agentId: "standard",
|
|
455
|
+
sessionId: "session-child",
|
|
456
|
+
parentSessionId: "session-parent"
|
|
457
|
+
});
|
|
458
|
+
assert.equal(out.source, "parentSession");
|
|
459
|
+
store.close();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("子 agent 自己有卡时自己的卡优先(继承不覆盖专门指定的卡)", () => {
|
|
463
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
464
|
+
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
|
|
465
|
+
settings.setPersonaForSession("session-child", { name: "子卡", enabled: true });
|
|
466
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
467
|
+
assert.equal(hit.source, "bySession");
|
|
468
|
+
assert.equal(hit.card.name, "子卡");
|
|
469
|
+
store.close();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("父卡优先于 byAgent(同一段对话比同一预设更能说明用谁的人格)", () => {
|
|
473
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
474
|
+
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
475
|
+
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
|
|
476
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
477
|
+
assert.equal(hit.source, "parentSession");
|
|
478
|
+
assert.equal(hit.card.name, "父卡");
|
|
479
|
+
store.close();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("父卡被显式关掉时不继承,落回 byAgent(关掉就是关掉)", () => {
|
|
483
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
484
|
+
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
485
|
+
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: false });
|
|
486
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
487
|
+
assert.equal(hit.source, "byAgent");
|
|
488
|
+
assert.equal(hit.card.name, "预设卡");
|
|
489
|
+
store.close();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("strict:子 agent 只解析到父会话 id 时也不许被 default 兜底", () => {
|
|
493
|
+
const { store, settings } = setup({ personaEnabled: true, personaStrictAddressing: true });
|
|
494
|
+
settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
|
|
495
|
+
// 子会话与父会话都没有卡;anyCard=true 且确实有身份 → none
|
|
496
|
+
const miss = resolvePersonaCard(settings, "standard", {
|
|
497
|
+
strict: true,
|
|
498
|
+
sessionId: "session-child",
|
|
499
|
+
parentSessionId: "session-nobody"
|
|
500
|
+
});
|
|
501
|
+
assert.equal(miss.source, "none", "有身份但没卡,strict 下不兜底");
|
|
502
|
+
store.close();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("strict 关时子 agent 没父卡照旧落 default(零行为变化)", () => {
|
|
506
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
507
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
508
|
+
settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
|
|
509
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-nope" });
|
|
510
|
+
assert.equal(hit.source, "default");
|
|
511
|
+
assert.equal(hit.card.name, "Amarre");
|
|
512
|
+
store.close();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("parentSession 与自身 id 相同(脏数据)时不重复查同一个键", () => {
|
|
516
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
517
|
+
settings.setPersonaForSession("session-same", { name: "同一张", enabled: true });
|
|
518
|
+
const hit = resolvePersonaCard(settings, "standard", {
|
|
519
|
+
sessionId: "session-same",
|
|
520
|
+
parentSessionId: "session-same"
|
|
521
|
+
});
|
|
522
|
+
assert.equal(hit.source, "bySession", "自身维度先命中,不会误标成继承");
|
|
523
|
+
store.close();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("父会话 id 是空白/非字符串时不影响其余寻址", () => {
|
|
527
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
528
|
+
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
529
|
+
for (const bad of ["", " ", null, undefined, 42, {}]) {
|
|
530
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: bad });
|
|
531
|
+
assert.equal(hit.source, "byAgent", `parentSessionId=${JSON.stringify(bad)} 应被忽略`);
|
|
532
|
+
}
|
|
533
|
+
store.close();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("session.header 取不到时不影响 agentPreset 寻址(新加的父会话读取也各自兜错)", () => {
|
|
537
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
538
|
+
seed(settings);
|
|
539
|
+
const hostile = {
|
|
540
|
+
agent: {
|
|
541
|
+
session: {
|
|
542
|
+
id: "session-x",
|
|
543
|
+
get header() { throw new Error("no header accessor"); }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const text = settingsCtx.text(hostile);
|
|
548
|
+
// header 整体取不到 → agentId/parentSessionId 皆 null,只剩 sessionId,落 default
|
|
549
|
+
assert.ok(text.includes("你的人格名字:Amarre"), "会话卡字段可用时不该整块崩掉");
|
|
550
|
+
store.close();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("注入链路端到端:子 agent 拿到父会话人格,且与预览同源", async () => {
|
|
554
|
+
const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
|
|
555
|
+
settings.setPersona({ name: "兜底", enabled: true });
|
|
556
|
+
settings.setPersonaForSession("session-parent", { name: "父会话人格", personality: "父的性格", enabled: true });
|
|
557
|
+
const fromInjector = injectorText({
|
|
558
|
+
agentPreset: "standard",
|
|
559
|
+
sessionId: "session-child",
|
|
560
|
+
parentSession: "session-parent"
|
|
561
|
+
});
|
|
562
|
+
assert.ok(fromInjector.includes("你的人格名字:父会话人格"));
|
|
563
|
+
const body = await preview({ agent: "standard", session: "session-child", parent: "session-parent" });
|
|
564
|
+
assert.equal(body.preview.text, fromInjector, "预览必须与注入器逐字节同源");
|
|
565
|
+
assert.equal(body.preview.source, "parentSession");
|
|
566
|
+
assert.equal(body.preview.parent, "session-parent");
|
|
567
|
+
store.close();
|
|
568
|
+
});
|