@modusensus/dsh-mneme 0.8.16 → 0.9.1
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 +91 -16
- package/assets/icon.svg +2 -2
- package/bin/cli.mjs +8 -2
- package/docs/CONFIGURATION.md +27 -1
- package/docs/CONTINUITY.md +18 -3
- package/docs/SEMANTIC.md +3 -3
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api-standalone.js +78 -12
- package/lib/api.js +403 -62
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +2294 -423
- package/lib/config.js +63 -6
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +17 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +5 -1
- package/lib/embedding.js +116 -15
- package/lib/entities/extractor.js +4 -0
- package/lib/index.js +81 -12
- package/lib/inject.js +40 -4
- package/lib/lang.js +124 -0
- package/lib/pipeline-activities.js +178 -0
- package/lib/pipeline-attribution.js +165 -0
- package/lib/pipeline-command.js +279 -0
- package/lib/pipeline-confirm.js +142 -0
- package/lib/pipeline-exec.js +174 -0
- package/lib/pipeline-nudge.js +1281 -0
- package/lib/pipeline-observations.js +181 -0
- package/lib/pipeline-observe.js +133 -0
- package/lib/pipeline-runner.js +346 -0
- package/lib/pipeline-runs.js +442 -0
- package/lib/pipeline.js +655 -0
- package/lib/scope.js +16 -4
- package/lib/semantic.js +66 -10
- package/lib/serve.js +1 -1
- package/lib/service.js +59 -18
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +138 -1
- package/lib/store.js +132 -6
- package/lib/summarize.js +30 -0
- package/lib/tools.js +11 -2
- package/package.json +3 -3
- package/scripts/changelog-prep.mjs +62 -7
- package/scripts/check-sync.js +8 -6
- package/scripts/publish-gate.mjs +197 -0
- package/scripts/release-docs.mjs +204 -0
- package/scripts/release-notes.mjs +162 -0
- package/src/api-standalone.js +78 -12
- package/src/api.js +403 -62
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +63 -6
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +17 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +5 -1
- package/src/embedding.js +116 -15
- package/src/entities/extractor.js +4 -0
- package/src/index.js +81 -12
- package/src/inject.js +40 -4
- package/src/lang.js +124 -0
- package/src/pipeline-activities.js +178 -0
- package/src/pipeline-attribution.js +165 -0
- package/src/pipeline-command.js +279 -0
- package/src/pipeline-confirm.js +142 -0
- package/src/pipeline-exec.js +174 -0
- package/src/pipeline-nudge.js +1281 -0
- package/src/pipeline-observations.js +181 -0
- package/src/pipeline-observe.js +133 -0
- package/src/pipeline-runner.js +346 -0
- package/src/pipeline-runs.js +442 -0
- package/src/pipeline.js +655 -0
- package/src/scope.js +16 -4
- package/src/semantic.js +66 -10
- package/src/serve.js +1 -1
- package/src/service.js +59 -18
- package/src/session-usage.js +350 -0
- package/src/settings.js +138 -1
- package/src/store.js +132 -6
- package/src/summarize.js +30 -0
- package/src/tools.js +11 -2
- package/test/api-hardening.test.js +173 -0
- package/test/api.test.js +244 -7
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/changelog-prep.test.js +74 -8
- package/test/cli-enums.test.js +37 -0
- package/test/client.test.js +510 -2
- package/test/continuity.test.js +84 -0
- package/test/docs-config-keys.test.js +65 -0
- package/test/embed-ready-poll.test.js +94 -0
- package/test/entities.test.js +13 -2
- package/test/failure-query.test.js +147 -0
- package/test/failure-retention.test.js +105 -0
- package/test/graph-api.test.js +36 -2
- package/test/helpers/write-admission-samples.js +6 -2
- package/test/inject-recall.test.js +32 -4
- package/test/lib-smoke.test.js +20 -0
- package/test/llm-audit.test.js +92 -1
- package/test/mint-source.test.js +202 -0
- package/test/persona-api.test.js +172 -0
- package/test/persona-flag.test.js +36 -0
- package/test/persona-inject.test.js +145 -0
- package/test/persona-settings.test.js +145 -0
- package/test/pipeline-activities.test.js +155 -0
- package/test/pipeline-attribution.test.js +175 -0
- package/test/pipeline-command.test.js +243 -0
- package/test/pipeline-confirm.test.js +148 -0
- package/test/pipeline-exec.test.js +172 -0
- package/test/pipeline-nudge.test.js +1036 -0
- package/test/pipeline-observations.test.js +157 -0
- package/test/pipeline-observe.test.js +127 -0
- package/test/pipeline-r1r2.test.js +149 -0
- package/test/pipeline-runner.test.js +442 -0
- package/test/pipeline-runs.test.js +767 -0
- package/test/pipeline.test.js +636 -0
- package/test/placement.test.js +1 -1
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/reindex-backfill.test.js +83 -4
- package/test/release-docs.test.js +130 -0
- package/test/release-notes.test.js +240 -0
- package/test/release-workflow.test.js +93 -0
- package/test/route-policy.test.js +113 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/security-doc.test.js +139 -0
- package/test/sensitive-scan.test.js +44 -0
- package/test/serve.test.js +1 -1
- package/test/session-usage.test.js +308 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/summarize.test.js +176 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
- package/test/workflow-guards.test.js +156 -0
package/test/llm-audit.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
import { mkdtempSync } from "node:fs";
|
|
5
|
+
import { mkdtempSync, readFileSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { Context } from "@deepseek-ai/cordis";
|
|
@@ -622,6 +622,97 @@ test("entity extraction writes no audit row when llmAudit.enabled === false", as
|
|
|
622
622
|
store.close();
|
|
623
623
|
});
|
|
624
624
|
|
|
625
|
+
// #405:实体抽取的审计行此前把失败原因压成一句常量、related_memory_ids 恒空,于是
|
|
626
|
+
// 429 / 配额 / 鉴权 / 超时在库里长得一模一样(主库 735 条 entity_extract error 行全
|
|
627
|
+
// 是「llm stream aborted or errored」),也查不出是哪条记忆没抽到实体。四条锁:原因
|
|
628
|
+
// 落进 error_message(复用 dream 的 describeStreamFailure,全仓归一只此一份)、无
|
|
629
|
+
// 细节时退回旧字面量、归属 id 传得进来且不凭空造、抛错路径同样带归属。
|
|
630
|
+
test("#405: entity extraction audit records the stream failure cause, not a constant", async () => {
|
|
631
|
+
const store = createStore(":memory:");
|
|
632
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
633
|
+
const streamEntityText = createEntityStreamAdapter({
|
|
634
|
+
llm: {
|
|
635
|
+
async *stream() {
|
|
636
|
+
yield {
|
|
637
|
+
type: "finish",
|
|
638
|
+
reason: { kind: "error", failure: { code: "rate_limited", message: "429 Too Many Requests", status: 429 } }
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
643
|
+
logger: { warn: () => {} },
|
|
644
|
+
service
|
|
645
|
+
});
|
|
646
|
+
const text = await streamEntityText(
|
|
647
|
+
[{ role: "user", content: [{ type: "text", text: "记忆内容" }] }],
|
|
648
|
+
{ memoryId: "mem-1" }
|
|
649
|
+
);
|
|
650
|
+
assert.equal(text, undefined, "a failed stream yields no text");
|
|
651
|
+
const row = service.listLlmAudits({ source: "entityExtract" })[0];
|
|
652
|
+
assert.equal(row.status, "error");
|
|
653
|
+
assert.equal(row.error_message, "rate_limited: 429 Too Many Requests",
|
|
654
|
+
"code + message from chunk.reason.failure survive into the audit row");
|
|
655
|
+
assert.notEqual(row.error_message, "llm stream aborted or errored",
|
|
656
|
+
"the constant that made every provider failure look identical is gone");
|
|
657
|
+
assert.deepEqual(row.related_memory_ids, ["mem-1"],
|
|
658
|
+
"the row is attributable to the memory that was being extracted");
|
|
659
|
+
store.close();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("#405: entity extraction audit keeps the legacy wording when the failure has no detail", async () => {
|
|
663
|
+
const store = createStore(":memory:");
|
|
664
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
665
|
+
const streamEntityText = createEntityStreamAdapter({
|
|
666
|
+
llm: {
|
|
667
|
+
async *stream() {
|
|
668
|
+
// 老形态:只有 kind,既没有 failure 也没有 message(dsh-llm 早期版本、或
|
|
669
|
+
// 替身)。这时退回旧字面量——把 error_message 写成空串在库里比一句常量更
|
|
670
|
+
// 难查,那等于用另一种方式把原因丢掉。
|
|
671
|
+
yield { type: "finish", reason: { kind: "aborted" } };
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
675
|
+
logger: { warn: () => {} },
|
|
676
|
+
service
|
|
677
|
+
});
|
|
678
|
+
await streamEntityText([{ role: "user", content: [{ type: "text", text: "记忆内容" }] }], {});
|
|
679
|
+
const row = service.listLlmAudits({ source: "entityExtract" })[0];
|
|
680
|
+
assert.equal(row.error_message, "llm stream aborted or errored");
|
|
681
|
+
assert.deepEqual(row.related_memory_ids, [], "no memoryId → still empty, never fabricated");
|
|
682
|
+
store.close();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("#405: entity extraction audit attributes a thrown stream error to the memory too", async () => {
|
|
686
|
+
const store = createStore(":memory:");
|
|
687
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
688
|
+
const streamEntityText = createEntityStreamAdapter({
|
|
689
|
+
llm: {
|
|
690
|
+
async *stream() {
|
|
691
|
+
throw new Error("socket hang up");
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
695
|
+
logger: { warn: () => {} },
|
|
696
|
+
service
|
|
697
|
+
});
|
|
698
|
+
await streamEntityText([{ role: "user", content: [{ type: "text", text: "记忆内容" }] }], { memoryId: "mem-9" });
|
|
699
|
+
const row = service.listLlmAudits({ source: "entityExtract" })[0];
|
|
700
|
+
assert.equal(row.status, "error");
|
|
701
|
+
assert.equal(row.error_message, "socket hang up");
|
|
702
|
+
assert.deepEqual(row.related_memory_ids, ["mem-9"], "the throw path attributes too");
|
|
703
|
+
store.close();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("#405: the failure-cause formatter stays a single implementation", async () => {
|
|
707
|
+
const indexSrc = readFileSync(new URL("../src/index.js", import.meta.url), "utf8");
|
|
708
|
+
const dreamSrc = readFileSync(new URL("../src/dream.js", import.meta.url), "utf8");
|
|
709
|
+
assert.match(indexSrc, /describeStreamFailure/, "index.js reuses dream.js's formatter");
|
|
710
|
+
// 有人在这里就地重写 code/message 拼接,就又多出一份口径——#411 的教训是「复制
|
|
711
|
+
// 粘贴漂移不是设计,是漂移」。锁死两端:归一逻辑只许在 dream.js 里存在一次。
|
|
712
|
+
assert.equal(indexSrc.match(/failure\.code/g), null, "index.js must not re-format the failure code itself");
|
|
713
|
+
assert.equal(dreamSrc.match(/function describeStreamFailure\(/g).length, 1, "exactly one definition");
|
|
714
|
+
});
|
|
715
|
+
|
|
625
716
|
// 启动期保留清理(Bug8)必须与写入侧读**同一份**配置。面板把 llmAudit.enabled 写进
|
|
626
717
|
// kv、经 nestedFlags 合进 cfg,bundle 里可能是关的:清理若读 rawCfg,就会出现「面板
|
|
627
718
|
// 说开 → 各写入点照写,raw 说关 → 不清理」这种无保留期的增长(写入准入 #254 也是读
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createStore } from "../src/store.js";
|
|
4
|
+
import { createService } from "../src/service.js";
|
|
5
|
+
import { applyDecisions, validateDecisions } from "../src/dream/decisions.js";
|
|
6
|
+
import { runSleep } from "../src/dream/sleep.js";
|
|
7
|
+
import { createDreamScheduler } from "../src/dream.js";
|
|
8
|
+
|
|
9
|
+
// 机器 mint 的记忆必须带真 source。
|
|
10
|
+
//
|
|
11
|
+
// 背景(现场实测,2026-10-14):pattern 表的 144 行 source 全是 "manual",
|
|
12
|
+
// 而按 dream_runs 的 patterns.applied 逐轮求和恰好也是 144——它们全是 sleep 的
|
|
13
|
+
// phasePatterns mint 的,那个 "manual" 只是 saveWithDedupe 没拿到 source 时的
|
|
14
|
+
// 兜底值。于是「这类条目从哪来」在数据面上不可读,靠它做的判断(service.js 里
|
|
15
|
+
// 那条「pattern 只由手工写入」)也就跟着错了。
|
|
16
|
+
//
|
|
17
|
+
// 这组用例锁两件事:① 三条 mint 管道各自声明自己的来源;② 来源只能由调用方给,
|
|
18
|
+
// 模型写在决策对象上的 source 一律不算数——validateDecisions 不剥离未知字段,
|
|
19
|
+
// 若 applyCreate 信了 d.source,模型就能给自己的输出盖「人写的」戳。
|
|
20
|
+
|
|
21
|
+
function setup() {
|
|
22
|
+
const store = createStore(":memory:");
|
|
23
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
24
|
+
return { store, service };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mint(service, extra = {}) {
|
|
28
|
+
return service
|
|
29
|
+
.saveWithDedupe({ type: "project", title: `铺垫${Math.random()}`, content: "给扫描窗垫一条普通记忆", importance: 3 })
|
|
30
|
+
.memory;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------- applyDecisions
|
|
34
|
+
|
|
35
|
+
test("mint source: applyDecisions stamps the caller's mintSource on create", () => {
|
|
36
|
+
const { store, service } = setup();
|
|
37
|
+
const { applied } = applyDecisions(
|
|
38
|
+
[{ action: "create", type: "pattern", title: "真模式", content: "从重复里抽出来的规律", importance: 3 }],
|
|
39
|
+
service,
|
|
40
|
+
null,
|
|
41
|
+
null,
|
|
42
|
+
{ mintSource: "sleep" }
|
|
43
|
+
);
|
|
44
|
+
assert.equal(applied, 1);
|
|
45
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
46
|
+
assert.equal(row.source, "sleep", "minted row carries the pipeline provenance");
|
|
47
|
+
store.close();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("mint source: dream pipeline stamps 'dream'", () => {
|
|
51
|
+
const { store, service } = setup();
|
|
52
|
+
applyDecisions(
|
|
53
|
+
[{ action: "create", type: "pattern", title: "真模式", content: "规律", importance: 3 }],
|
|
54
|
+
service,
|
|
55
|
+
null,
|
|
56
|
+
null,
|
|
57
|
+
{ mintSource: "dream" }
|
|
58
|
+
);
|
|
59
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
60
|
+
assert.equal(row.source, "dream");
|
|
61
|
+
store.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 对抗条:来源取自调用方,不取自决策对象。模型自报的 source 必须被忽略——
|
|
65
|
+
// 否则「机器写的不冒充人写的」这条防线就形同虚设。
|
|
66
|
+
test("mint source: a source on the decision object is ignored, pipeline wins", () => {
|
|
67
|
+
const { store, service } = setup();
|
|
68
|
+
const decisions = [
|
|
69
|
+
{ action: "create", type: "pattern", title: "真模式", content: "规律", importance: 3, source: "manual" }
|
|
70
|
+
];
|
|
71
|
+
// 先证明威胁真实存在:这类决策能整个通过校验走到 applyCreate(校验不剥离未知
|
|
72
|
+
// 字段)。若哪天真加了剥离,这条会红——那时下面的对抗断言也就不再承重了。
|
|
73
|
+
const { ok } = validateDecisions(decisions, new Map(), { maxCreatePerRun: 3 });
|
|
74
|
+
assert.equal(ok, true, "a create carrying its own source still validates — that is the threat");
|
|
75
|
+
|
|
76
|
+
applyDecisions(decisions, service, null, null, { mintSource: "sleep" });
|
|
77
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
78
|
+
assert.equal(row.source, "sleep", "the model-authored source must not win");
|
|
79
|
+
store.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 反向:模型自报 source 而没有管道来源时,也不许它自己说了算——回落到兜底,
|
|
83
|
+
// 而不是让模型指哪打哪。
|
|
84
|
+
test("mint source: without a pipeline source the model-authored one still does not win", () => {
|
|
85
|
+
const { store, service } = setup();
|
|
86
|
+
applyDecisions(
|
|
87
|
+
[{ action: "create", type: "pattern", title: "真模式", content: "规律", importance: 3, source: "human" }],
|
|
88
|
+
service
|
|
89
|
+
);
|
|
90
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
91
|
+
assert.equal(row.source, "manual", "falls back to the existing default, not the model's claim");
|
|
92
|
+
store.close();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 不传 mintSource 时行为与改前逐字一致(不放大行为面)。
|
|
96
|
+
test("mint source: absent mintSource keeps the previous fallback", () => {
|
|
97
|
+
const { store, service } = setup();
|
|
98
|
+
applyDecisions([{ action: "create", type: "pattern", title: "真模式", content: "规律", importance: 3 }], service);
|
|
99
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
100
|
+
assert.equal(row.source, "manual");
|
|
101
|
+
store.close();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 空白串按「没给」处理,不能写出 source="" 这种假装有来源的空值。
|
|
105
|
+
test("mint source: blank mintSource is treated as absent", () => {
|
|
106
|
+
const { store, service } = setup();
|
|
107
|
+
applyDecisions(
|
|
108
|
+
[{ action: "create", type: "pattern", title: "真模式", content: "规律", importance: 3 }],
|
|
109
|
+
service,
|
|
110
|
+
null,
|
|
111
|
+
null,
|
|
112
|
+
{ mintSource: " " }
|
|
113
|
+
);
|
|
114
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
115
|
+
assert.equal(row.source, "manual");
|
|
116
|
+
store.close();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ------------------------------------------------------------------ end to end
|
|
120
|
+
|
|
121
|
+
// sleep 的 pattern 相位(唯一的真实 mint 路径)端到端:断言落库行的 source。
|
|
122
|
+
// 不能靠等下一次做梦验收(要调度 + LLM 路由),所以在这里驱动相位。
|
|
123
|
+
function sleepCtx(decisionsJson) {
|
|
124
|
+
return {
|
|
125
|
+
logger: { warn: () => {}, info: () => {} },
|
|
126
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "sleep-model" }) },
|
|
127
|
+
llm: {
|
|
128
|
+
async *stream(options) {
|
|
129
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
130
|
+
yield { type: "text-delta", index: 0, text: userText.startsWith("id=") ? decisionsJson : "[]" };
|
|
131
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
test("mint source: sleep pattern phase mints with source=sleep", async () => {
|
|
138
|
+
const { store, service } = setup();
|
|
139
|
+
mint(service);
|
|
140
|
+
mint(service);
|
|
141
|
+
const ctx = sleepCtx(
|
|
142
|
+
JSON.stringify([{ action: "create", type: "pattern", title: "真模式", content: "从 a 和 b 提取", importance: 3 }])
|
|
143
|
+
);
|
|
144
|
+
const config = {
|
|
145
|
+
sleepModeEnabled: true,
|
|
146
|
+
sleepPatternMinMemories: 10,
|
|
147
|
+
sleepMaxPatternPerRun: 3
|
|
148
|
+
};
|
|
149
|
+
const result = await runSleep(ctx, service, config, ctx.logger, null, null);
|
|
150
|
+
assert.equal(result.phases.patterns.status, "ok", "pattern phase minted");
|
|
151
|
+
|
|
152
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
153
|
+
assert.ok(row, "pattern minted");
|
|
154
|
+
assert.equal(row.source, "sleep", "the sleep phase declares its own provenance");
|
|
155
|
+
store.close();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("mint source: dream main chain mints with source=dream", async () => {
|
|
159
|
+
const { store, service } = setup();
|
|
160
|
+
const seed = service.saveWithDedupe({ type: "project", title: "旧条目", content: "第一段内容", importance: 3 }).memory;
|
|
161
|
+
let call = 0;
|
|
162
|
+
const ctx = {
|
|
163
|
+
logger: { warn: () => {}, info: () => {} },
|
|
164
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
165
|
+
llm: {
|
|
166
|
+
async *stream(options) {
|
|
167
|
+
call++;
|
|
168
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
169
|
+
const text = call === 1
|
|
170
|
+
? JSON.stringify([
|
|
171
|
+
{ action: "keep", ids: [seed.id] },
|
|
172
|
+
{ action: "create", type: "pattern", title: "主链路模式", content: "巩固时发现的规律", importance: 3 }
|
|
173
|
+
])
|
|
174
|
+
: "记忆库总览摘要文本";
|
|
175
|
+
void userText;
|
|
176
|
+
yield { type: "text-delta", text };
|
|
177
|
+
yield { type: "finish", reason: { kind: "ok" } };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
182
|
+
const result = await dream.runDream(ctx, service, { dreamProvider: "mock", dreamModel: "mock-model" });
|
|
183
|
+
assert.equal(result.ok, true);
|
|
184
|
+
|
|
185
|
+
const row = service.all().find((m) => m.type === "pattern");
|
|
186
|
+
assert.ok(row, "the create decision minted a row");
|
|
187
|
+
assert.equal(row.source, "dream");
|
|
188
|
+
store.close();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// 验收口径本身:全表分组统计随之可读——同一次运行 mint 的条目能按来源数出来。
|
|
192
|
+
test("mint source: rows are countable by source after minting", () => {
|
|
193
|
+
const { store, service } = setup();
|
|
194
|
+
applyDecisions([{ action: "create", type: "pattern", title: "甲", content: "规律甲", importance: 3 }], service, null, null, { mintSource: "sleep" });
|
|
195
|
+
applyDecisions([{ action: "create", type: "pattern", title: "乙", content: "规律乙", importance: 3 }], service, null, null, { mintSource: "dream" });
|
|
196
|
+
const bySource = new Map();
|
|
197
|
+
for (const m of service.all()) bySource.set(m.source, (bySource.get(m.source) ?? 0) + 1);
|
|
198
|
+
assert.equal(bySource.get("sleep"), 1);
|
|
199
|
+
assert.equal(bySource.get("dream"), 1);
|
|
200
|
+
assert.equal(bySource.get("manual"), undefined, "nothing minted here fell back to the default");
|
|
201
|
+
store.close();
|
|
202
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Persona API routes: internal (/api/dsh-mneme/persona, requireAuth on write)
|
|
2
|
+
// and standalone (/persona behind the global Bearer gate).
|
|
3
|
+
// Locks: GET reads current value; unauthorized writes are rejected; a corrupt
|
|
4
|
+
// JSON body never lands in the store (design: reports/persona-page-design.md ②).
|
|
5
|
+
// Call shapes mirror test/api.test.js (route registry + FakeRes) and
|
|
6
|
+
// test/standalone-api.test.js (real server on port 0 + fetch).
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { createStore } from "../src/store.js";
|
|
11
|
+
import { createService } from "../src/service.js";
|
|
12
|
+
import { createApi } from "../src/api.js";
|
|
13
|
+
import { createSettings } from "../src/settings.js";
|
|
14
|
+
import { createStandaloneApi } from "../src/api-standalone.js";
|
|
15
|
+
|
|
16
|
+
class FakeRes extends EventEmitter {
|
|
17
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
18
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
19
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function req(path, method = "GET", body = null, headers = {}) {
|
|
23
|
+
const r = new EventEmitter();
|
|
24
|
+
r.url = path;
|
|
25
|
+
r.method = method;
|
|
26
|
+
r.headers = headers;
|
|
27
|
+
if (body !== null) {
|
|
28
|
+
process.nextTick(() => {
|
|
29
|
+
r.emit("data", Buffer.from(typeof body === "string" ? body : JSON.stringify(body)));
|
|
30
|
+
r.emit("end");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setupInternal(apiToken = "") {
|
|
37
|
+
const store = createStore(":memory:");
|
|
38
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
39
|
+
const settings = createSettings(store.db);
|
|
40
|
+
const commands = {
|
|
41
|
+
add: (def) => settings.addCommand(def),
|
|
42
|
+
remove: (id) => settings.removeCommand(id),
|
|
43
|
+
list: () => settings.listCommands()
|
|
44
|
+
};
|
|
45
|
+
const routes = [];
|
|
46
|
+
const ctx = {
|
|
47
|
+
webServer: {
|
|
48
|
+
register(route) {
|
|
49
|
+
routes.push(route);
|
|
50
|
+
return () => {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
createApi(ctx, service, settings, commands, undefined, undefined, apiToken, {});
|
|
55
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/persona");
|
|
56
|
+
return { store, settings, route, apiToken };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- internal API(宿主内 /api/dsh-mneme/*) --------------------------------
|
|
60
|
+
|
|
61
|
+
test("internal GET /persona returns current persona without auth", async () => {
|
|
62
|
+
const { store, settings, route } = setupInternal();
|
|
63
|
+
settings.setPersona({ name: "Amarre", enabled: false });
|
|
64
|
+
const res = new FakeRes();
|
|
65
|
+
await route.handler(req("/api/dsh-mneme/persona"), res);
|
|
66
|
+
assert.equal(res.statusCode, 200);
|
|
67
|
+
assert.equal(JSON.parse(res.body).persona.default.name, "Amarre");
|
|
68
|
+
store.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("internal PUT /persona without token is rejected and store untouched", async () => {
|
|
72
|
+
// setupInternal("t"):必须带 token 启动,无 token 的写请求才应被拒
|
|
73
|
+
// (apiToken 为空时 isAuthorized 恒放行——与 /profile 的既有语义一致)。
|
|
74
|
+
const { store, settings, route } = setupInternal("t");
|
|
75
|
+
const res = new FakeRes();
|
|
76
|
+
await route.handler(req("/api/dsh-mneme/persona", "PUT", { persona: { name: "evil" } }), res);
|
|
77
|
+
assert.equal(res.statusCode, 401);
|
|
78
|
+
assert.deepEqual(settings.getPersona(), { default: {}, byAgent: {} });
|
|
79
|
+
store.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("internal PUT /persona with token round-trips; corrupt JSON body does not land", async () => {
|
|
83
|
+
const { store, settings, route } = setupInternal("t");
|
|
84
|
+
const headers = { authorization: "Bearer t" };
|
|
85
|
+
const ok = new FakeRes();
|
|
86
|
+
await route.handler(req("/api/dsh-mneme/persona", "PUT", { persona: { name: "Amarre", enabled: true } }, headers), ok);
|
|
87
|
+
assert.equal(ok.statusCode, 200);
|
|
88
|
+
assert.equal(settings.getPersona().default.name, "Amarre");
|
|
89
|
+
const bad = new FakeRes();
|
|
90
|
+
await route.handler(req("/api/dsh-mneme/persona", "PUT", "not json", headers), bad);
|
|
91
|
+
// 路由层对坏 JSON 容错(parseBody 口径:坏体不清空库内值),原样回显现值
|
|
92
|
+
assert.equal(bad.statusCode, 200);
|
|
93
|
+
assert.equal(settings.getPersona().default.name, "Amarre");
|
|
94
|
+
store.close();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("internal PUT /persona multi-card form replaces fully; legacy form merges default", async () => {
|
|
98
|
+
const { store, settings, route } = setupInternal("t");
|
|
99
|
+
const headers = { authorization: "Bearer t" };
|
|
100
|
+
// 先铺一张 byAgent 卡
|
|
101
|
+
settings.setPersonaFor("zephyr", { name: "泽菲尔", enabled: true });
|
|
102
|
+
// 旧形 PUT(单对象):只动 default、byAgent 保留
|
|
103
|
+
const legacy = new FakeRes();
|
|
104
|
+
await route.handler(req("/api/dsh-mneme/persona", "PUT", { persona: { name: "Amarre-改", enabled: true } }, headers), legacy);
|
|
105
|
+
assert.equal(legacy.statusCode, 200);
|
|
106
|
+
assert.equal(settings.getPersona().default.name, "Amarre-改");
|
|
107
|
+
assert.equal(settings.getPersona().byAgent.zephyr.name, "泽菲尔", "legacy PUT keeps byAgent");
|
|
108
|
+
// 新形 PUT(多卡):全量替换
|
|
109
|
+
const multi = new FakeRes();
|
|
110
|
+
await route.handler(req("/api/dsh-mneme/persona", "PUT", { persona: { default: { name: "默认卡", enabled: true }, byAgent: { zephyr: { name: "泽菲尔-新", enabled: true } } } }, headers), multi);
|
|
111
|
+
assert.equal(multi.statusCode, 200);
|
|
112
|
+
const got = settings.getPersona();
|
|
113
|
+
assert.equal(got.default.name, "默认卡");
|
|
114
|
+
assert.equal(got.byAgent.zephyr.name, "泽菲尔-新");
|
|
115
|
+
store.close();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---- standalone API(Bearer 数据面,真端口 + fetch) ------------------------
|
|
119
|
+
|
|
120
|
+
async function setupStandalone() {
|
|
121
|
+
const store = createStore(":memory:");
|
|
122
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
123
|
+
const settings = createSettings(store.db);
|
|
124
|
+
const api = createStandaloneApi({ service, store, config: {}, settings, logger: null, port: 0 });
|
|
125
|
+
await api.ready;
|
|
126
|
+
const base = `http://127.0.0.1:${api.port}`;
|
|
127
|
+
const auth = { authorization: `Bearer ${api.token}` };
|
|
128
|
+
return {
|
|
129
|
+
store, settings, base, auth,
|
|
130
|
+
close: () => {
|
|
131
|
+
api.server.closeIdleConnections?.();
|
|
132
|
+
api.server.close();
|
|
133
|
+
store.close();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test("standalone GET/PUT /persona behind Bearer gate round-trips", async () => {
|
|
139
|
+
const t = await setupStandalone();
|
|
140
|
+
try {
|
|
141
|
+
const get1 = await fetch(`${t.base}/persona`, { headers: t.auth });
|
|
142
|
+
assert.equal(get1.status, 200);
|
|
143
|
+
assert.deepEqual((await get1.json()).persona, { default: {}, byAgent: {} });
|
|
144
|
+
const put = await fetch(`${t.base}/persona`, {
|
|
145
|
+
method: "PUT",
|
|
146
|
+
headers: { ...t.auth, "content-type": "application/json" },
|
|
147
|
+
body: JSON.stringify({ persona: { name: "Zelphi", enabled: false } })
|
|
148
|
+
});
|
|
149
|
+
assert.equal(put.status, 200);
|
|
150
|
+
assert.equal((await put.json()).persona.default.name, "Zelphi");
|
|
151
|
+
const bad = await fetch(`${t.base}/persona`, {
|
|
152
|
+
method: "PUT",
|
|
153
|
+
headers: { ...t.auth, "content-type": "application/json" },
|
|
154
|
+
body: "not json"
|
|
155
|
+
});
|
|
156
|
+
assert.equal(bad.status, 400);
|
|
157
|
+
assert.equal(t.settings.getPersona().default.name, "Zelphi");
|
|
158
|
+
} finally {
|
|
159
|
+
t.close();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("standalone /persona without Bearer is 401", async () => {
|
|
164
|
+
const t = await setupStandalone();
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(`${t.base}/persona`, { method: "PUT", body: "{}" });
|
|
167
|
+
assert.equal(res.status, 401);
|
|
168
|
+
assert.deepEqual(t.settings.getPersona(), { default: {}, byAgent: {} });
|
|
169
|
+
} finally {
|
|
170
|
+
t.close();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// personaEnabled flag wiring (#254-style): schema key + whitelist pair must
|
|
2
|
+
// surface in /features effective with default false — a missing key would make
|
|
3
|
+
// the panel toggle silently ineffective (client reads effective to register).
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { Config } from "../src/config.js";
|
|
7
|
+
import { createSettings, FEATURE_FLAG_SPEC } from "../src/settings.js";
|
|
8
|
+
import { createStore } from "../src/store.js";
|
|
9
|
+
|
|
10
|
+
test("config schema has personaEnabled defaulting to false", () => {
|
|
11
|
+
const cfg = Config({});
|
|
12
|
+
assert.equal(cfg.personaEnabled, false);
|
|
13
|
+
// 顶层扁平布尔:显式 true 穿透
|
|
14
|
+
const cfgOn = Config({ personaEnabled: true });
|
|
15
|
+
assert.equal(cfgOn.personaEnabled, true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("personaEnabled is in the boolean whitelist and spec export", () => {
|
|
19
|
+
assert.ok(FEATURE_FLAG_SPEC.booleans.includes("personaEnabled"));
|
|
20
|
+
// 默认关:不在 lightMode 排除名单(轻量纯文本注入物,用户显式勾选赢——
|
|
21
|
+
// 定性同 actionPipelineEnabled;若未来要进名单,此处会失败并提示改设计稿④)
|
|
22
|
+
const cfg = Config({});
|
|
23
|
+
const light = cfg.lightMode === true ? cfg : cfg;
|
|
24
|
+
void light; // lightMode 定性由 api.test.js 计数锁与 config.js 注释共同钉住
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("effective snapshot includes personaEnabled (false default) via featureSnapshot path", () => {
|
|
28
|
+
const store = createStore(":memory:");
|
|
29
|
+
const settings = createSettings(store.db);
|
|
30
|
+
// settings.setFeatureFlags 逐键校验合并写:白名单键可写(w1 已锁 TypeError 面)
|
|
31
|
+
settings.setFeatureFlags({ personaEnabled: true });
|
|
32
|
+
assert.equal(settings.getFeatureFlags().personaEnabled, true);
|
|
33
|
+
// 清掉覆盖后 effective 应回落到 config 默认 false——由 api.test.js 的
|
|
34
|
+
// 计数锁 + personaEnabled 默认值断言在 /features 路径上完整验证(w1 已同步 +1)。
|
|
35
|
+
store.close();
|
|
36
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Persona injection via renderUserSettings (user-settings context, order 85).
|
|
2
|
+
// Locks: with either gate off, output is byte-identical to the legacy rendering
|
|
3
|
+
// (old users see zero change); with both gates on, persona lines lead the block
|
|
4
|
+
// (design ⑥: persona → profile → rules, rules stay hardest) and go through
|
|
5
|
+
// escapePromptVars.
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { createStore } from "../src/store.js";
|
|
9
|
+
import { createService } from "../src/service.js";
|
|
10
|
+
import { createInjector } from "../src/inject.js";
|
|
11
|
+
import { createSettings } from "../src/settings.js";
|
|
12
|
+
|
|
13
|
+
function setup(over = {}) {
|
|
14
|
+
const store = createStore(":memory:");
|
|
15
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
16
|
+
const settings = createSettings(store.db);
|
|
17
|
+
const contexts = [];
|
|
18
|
+
const ctx = {
|
|
19
|
+
systemPrompt: {
|
|
20
|
+
context(def) {
|
|
21
|
+
contexts.push(def);
|
|
22
|
+
return () => {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const config = { maxInjectedItems: 3, importanceThreshold: 3, ...over };
|
|
27
|
+
createInjector(ctx, service, settings, config);
|
|
28
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
29
|
+
return { store, settings, settingsCtx };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("legacy output unchanged when config gate off (persona stored but ignored)", () => {
|
|
33
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: false });
|
|
34
|
+
settings.setProfile("用户是规划背景");
|
|
35
|
+
settings.setRules(["不提及用户名"]);
|
|
36
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true });
|
|
37
|
+
const text = settingsCtx.text({});
|
|
38
|
+
// 逐字节等于历史渲染:无人格行、无人格名
|
|
39
|
+
assert.equal(text, escapeless([
|
|
40
|
+
"[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
41
|
+
"- 用户画像:用户是规划背景",
|
|
42
|
+
"- 规则:不提及用户名"
|
|
43
|
+
].join("\n")));
|
|
44
|
+
store.close();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("legacy output unchanged when persona.enabled off (config gate on)", () => {
|
|
48
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
49
|
+
settings.setProfile("用户是规划背景");
|
|
50
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: false });
|
|
51
|
+
const text = settingsCtx.text({});
|
|
52
|
+
assert.ok(text.includes("用户画像"));
|
|
53
|
+
assert.ok(!text.includes("人格"));
|
|
54
|
+
store.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("empty block stays empty when only persona exists but a gate is off", () => {
|
|
58
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: false });
|
|
59
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
60
|
+
assert.equal(settingsCtx.text({}), "");
|
|
61
|
+
store.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("both gates on: persona lines lead, then profile, then rules", () => {
|
|
65
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
66
|
+
settings.setProfile("用户是规划背景");
|
|
67
|
+
settings.setRules(["不提及用户名"]);
|
|
68
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", replyStyle: "先给结论再解释", enabled: true });
|
|
69
|
+
const text = settingsCtx.text({});
|
|
70
|
+
const idxPersona = text.indexOf("你的人格名字:Amarre");
|
|
71
|
+
const idxProfile = text.indexOf("用户画像:用户是规划背景");
|
|
72
|
+
const idxRule = text.indexOf("规则:不提及用户名");
|
|
73
|
+
assert.ok(idxPersona > -1, "persona line present");
|
|
74
|
+
assert.ok(idxProfile > idxPersona, "profile after persona (design ⑥)");
|
|
75
|
+
assert.ok(idxRule > idxProfile, "rules after profile (rules hardest)");
|
|
76
|
+
assert.ok(text.includes("人格性格设定:轻松幽默"));
|
|
77
|
+
assert.ok(text.includes("回复要求:先给结论再解释"));
|
|
78
|
+
store.close();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("persona with blank text fields renders no persona lines (no empty bullets)", () => {
|
|
82
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
83
|
+
settings.setPersona({ name: " ", personality: "", enabled: true });
|
|
84
|
+
assert.equal(settingsCtx.text({}), "");
|
|
85
|
+
settings.setProfile("有画像");
|
|
86
|
+
const text = settingsCtx.text({});
|
|
87
|
+
assert.ok(!text.includes("人格"));
|
|
88
|
+
store.close();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("persona text goes through escapePromptVars (no raw braces)", () => {
|
|
92
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
93
|
+
settings.setPersona({ name: "{{evil}}", enabled: true });
|
|
94
|
+
const text = settingsCtx.text({});
|
|
95
|
+
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
96
|
+
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
97
|
+
store.close();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---- v1.2 多卡寻址:按会话身份取卡,取不到落 default ------------------------
|
|
101
|
+
|
|
102
|
+
test("session identity hit: byAgent card wins over default", () => {
|
|
103
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
104
|
+
settings.setPersona({ name: "Amarre", enabled: true }); // default
|
|
105
|
+
settings.setPersonaFor("zephyr", { name: "泽菲尔", personality: "冷危占有", enabled: true });
|
|
106
|
+
const ctxWithId = { agent: { session: { header: { agentPreset: "zephyr" } } } };
|
|
107
|
+
const text = settingsCtx.text(ctxWithId);
|
|
108
|
+
assert.ok(text.includes("你的人格名字:泽菲尔"), "byAgent card injected");
|
|
109
|
+
assert.ok(!text.includes("Amarre"), "default card not injected when identity hit");
|
|
110
|
+
store.close();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("session identity miss: falls back to default card", () => {
|
|
114
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
115
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
116
|
+
settings.setPersonaFor("zephyr", { name: "泽菲尔", enabled: true });
|
|
117
|
+
const ctxUnknown = { agent: { session: { header: { agentPreset: "unknown-bot" } } } };
|
|
118
|
+
const text = settingsCtx.text(ctxUnknown);
|
|
119
|
+
assert.ok(text.includes("Amarre"), "default on identity miss");
|
|
120
|
+
assert.ok(!text.includes("泽菲尔"), "other card not leaked");
|
|
121
|
+
store.close();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("identity card disabled (enabled=false) falls back to default, not partial card", () => {
|
|
125
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
126
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
127
|
+
settings.setPersonaFor("zephyr", { name: "泽菲尔", personality: "冷", enabled: false });
|
|
128
|
+
const ctxWithId = { agent: { session: { header: { agentPreset: "zephyr" } } } };
|
|
129
|
+
const text = settingsCtx.text(ctxWithId);
|
|
130
|
+
assert.ok(text.includes("Amarre"), "disabled id card → default");
|
|
131
|
+
assert.ok(!text.includes("泽菲尔"), "disabled card never partially injected");
|
|
132
|
+
store.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("session without header (mock/old host) behaves exactly like v1 (default)", () => {
|
|
136
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
137
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true });
|
|
138
|
+
const text = settingsCtx.text({}); // 无 session.header → default,与 v1 一致
|
|
139
|
+
assert.ok(text.includes("你的人格名字:Amarre"));
|
|
140
|
+
assert.ok(text.includes("人格性格设定:轻松幽默"));
|
|
141
|
+
store.close();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 期望值工具:直接写明文(本组用例不含需转义字符,escapePromptVars 为恒等)。
|
|
145
|
+
function escapeless(s) { return s; }
|