@modusensus/dsh-mneme 0.7.13 → 0.7.15
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.en.md +2 -2
- package/README.md +7 -5
- package/lib/api.js +338 -7
- package/lib/client.js +1238 -204
- package/lib/index.js +25 -2
- package/lib/mirror.js +93 -59
- package/lib/service.js +2 -0
- package/lib/settings.js +174 -0
- package/lib/store.js +77 -4
- package/lib/summarize.js +8 -8
- package/package.json +12 -1
- package/scripts/check-sync.js +42 -0
- package/src/api.js +338 -7
- package/src/index.js +25 -2
- package/src/mirror.js +93 -59
- package/src/service.js +2 -0
- package/src/settings.js +174 -0
- package/src/store.js +77 -4
- package/src/summarize.js +8 -8
- package/test/api.test.js +485 -2
- package/test/client.test.js +82 -52
- package/test/helpers/peer-worker.mjs +17 -2
- package/test/peer-blockers.test.js +9 -1
- package/test/settings.test.js +136 -0
- package/test/summarize.test.js +28 -0
package/test/client.test.js
CHANGED
|
@@ -41,71 +41,71 @@ test("memory entry registers into the sidebar foot slot", () => {
|
|
|
41
41
|
);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
// The
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
test("tab-first activation with hero-screen overlay fallback", () => {
|
|
44
|
+
// The tab era is over: the in-conversation memory tab fought the floating
|
|
45
|
+
// composer and squeezed the session layout, so the library no longer
|
|
46
|
+
// registers as a conversation view. The sidebar entry opens the centered
|
|
47
|
+
// sheet directly — from any state, including the new-chat hero screen. A
|
|
48
|
+
// dialog role stays banned either way.
|
|
49
|
+
test("sidebar entry opens the sheet; conversation-view tab stays retired", () => {
|
|
51
50
|
assert.equal(
|
|
52
51
|
clientSource.includes("role: \"dialog\""),
|
|
53
52
|
false,
|
|
54
53
|
"no dialog surface should remain"
|
|
55
54
|
);
|
|
56
55
|
assert.ok(
|
|
57
|
-
|
|
58
|
-
"the
|
|
56
|
+
/function openLibrary\(\) \{\s*setOverlayOpen\(true\);\s*\}/.test(clientSource),
|
|
57
|
+
"the entry must open the overlay directly (no tab activation)"
|
|
59
58
|
);
|
|
60
|
-
assert.
|
|
61
|
-
clientSource.includes(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
assert.ok(
|
|
65
|
-
/if \(candidates\.length === 0\) \{ resolve\(false\); return; \}/.test(clientSource),
|
|
66
|
-
"activation must resolve false when the tab ring is absent (hero screen)"
|
|
59
|
+
assert.equal(
|
|
60
|
+
clientSource.includes('ctx.slots.inject("conversation.view"'),
|
|
61
|
+
false,
|
|
62
|
+
"the conversation.view tab must stay retired"
|
|
67
63
|
);
|
|
68
|
-
assert.
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
assert.equal(
|
|
65
|
+
clientSource.includes("activateExplorerTab"),
|
|
66
|
+
false,
|
|
67
|
+
"the tab-click activation machinery must be gone"
|
|
71
68
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// The sheet is deliberately NOT fullscreen: a dimmed backdrop plus a rounded
|
|
72
|
+
// panel capped at 1180×880 keeps the conversation visible behind it. It
|
|
73
|
+
// reuses the full MemoryExplorer and closes on Esc, the close button, or a
|
|
74
|
+
// backdrop click.
|
|
75
|
+
test("memory library opens as a centered sheet with backdrop", () => {
|
|
75
76
|
assert.ok(
|
|
76
|
-
|
|
77
|
-
"
|
|
77
|
+
clientSource.includes(".mneme-backdrop{position:fixed;inset:0"),
|
|
78
|
+
"a dimmed backdrop must sit behind the sheet"
|
|
78
79
|
);
|
|
79
80
|
assert.ok(
|
|
80
|
-
|
|
81
|
-
"
|
|
81
|
+
/\.mneme-overlay\{position:fixed;z-index:1000;left:50%;top:50%;transform:translate\(-50%,-50%\)/.test(clientSource),
|
|
82
|
+
"the sheet must be centered, not viewport-filling"
|
|
82
83
|
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// The fallback overlay reuses the full MemoryExplorer (three columns, graph,
|
|
86
|
-
// settings) at viewport size — not the old side drawer — and closes on Esc
|
|
87
|
-
// or the close button.
|
|
88
|
-
test("hero fallback overlay is a full-viewport MemoryExplorer with a close affordance", () => {
|
|
89
84
|
assert.ok(
|
|
90
|
-
clientSource.includes(
|
|
91
|
-
"the
|
|
85
|
+
clientSource.includes("width:min(1240px,calc(100vw - 88px))"),
|
|
86
|
+
"the sheet must not occupy the full width and keeps friendly margins"
|
|
92
87
|
);
|
|
93
88
|
assert.ok(
|
|
94
89
|
clientSource.includes('h(MemoryExplorer, { t })'),
|
|
95
|
-
"the
|
|
90
|
+
"the sheet renders the same MemoryExplorer component"
|
|
96
91
|
);
|
|
97
92
|
assert.ok(
|
|
98
93
|
clientSource.includes('e.key === "Escape"'),
|
|
99
|
-
"Esc must close the
|
|
94
|
+
"Esc must close the sheet"
|
|
95
|
+
);
|
|
96
|
+
assert.ok(
|
|
97
|
+
clientSource.includes('className: "mneme-backdrop"'),
|
|
98
|
+
"the backdrop is a clickable close surface"
|
|
100
99
|
);
|
|
101
100
|
assert.ok(
|
|
102
101
|
clientSource.includes('"memory.overlay.close"'),
|
|
103
|
-
"the
|
|
102
|
+
"the close affordance keeps its localized label"
|
|
104
103
|
);
|
|
105
104
|
});
|
|
106
105
|
|
|
107
|
-
// The sidebar hands each
|
|
108
|
-
//
|
|
106
|
+
// The sidebar hands each entry only its column state: a wide row (icon +
|
|
107
|
+
// label) when expanded, a bare rail icon when collapsed — for both the
|
|
108
|
+
// portalled top button and the footer fallback.
|
|
109
109
|
test("trigger renders a wide row or a rail icon from the wide flag", () => {
|
|
110
110
|
assert.ok(
|
|
111
111
|
/wide \? "mneme-trigger" : "mneme-trigger mneme-rail"/.test(clientSource),
|
|
@@ -115,23 +115,48 @@ test("trigger renders a wide row or a rail icon from the wide flag", () => {
|
|
|
115
115
|
/wide && h\("span", \{ className: "mneme-trigger-label" \}/.test(clientSource),
|
|
116
116
|
"the label span must render only when wide"
|
|
117
117
|
);
|
|
118
|
+
assert.ok(
|
|
119
|
+
/size: wide \? 15 : 18/.test(clientSource),
|
|
120
|
+
"the portalled icon must follow the native rail sizing convention"
|
|
121
|
+
);
|
|
118
122
|
});
|
|
119
123
|
|
|
120
|
-
// The
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
+
// The entry lives ABOVE the workspaces region: the real button is portalled
|
|
125
|
+
// just before the host's regionArea container (below New Session, above the
|
|
126
|
+
// workspace list), while the sidebar.footer.action registration remains as
|
|
127
|
+
// the React anchor and the in-place fallback when the host markup changes.
|
|
128
|
+
test("sidebar entry portals above the workspaces region with footer fallback", () => {
|
|
124
129
|
assert.ok(
|
|
125
|
-
clientSource.includes('ctx.slots.inject("
|
|
126
|
-
"
|
|
130
|
+
clientSource.includes('ctx.slots.inject("sidebar.footer.action"'),
|
|
131
|
+
"the footer slot registration must stay as anchor + fallback"
|
|
132
|
+
);
|
|
133
|
+
assert.ok(
|
|
134
|
+
clientSource.includes("SidebarTopEntry"),
|
|
135
|
+
"the portalled top entry must exist"
|
|
136
|
+
);
|
|
137
|
+
assert.ok(
|
|
138
|
+
clientSource.includes(`'[class*="regionArea"]'`),
|
|
139
|
+
"the portal must anchor at the host's regionArea container"
|
|
127
140
|
);
|
|
128
141
|
assert.ok(
|
|
129
|
-
clientSource.includes(
|
|
130
|
-
"the
|
|
142
|
+
clientSource.includes("insertBefore(created, region)"),
|
|
143
|
+
"the entry must sit immediately above the workspaces region"
|
|
144
|
+
);
|
|
145
|
+
assert.ok(
|
|
146
|
+
clientSource.includes("if (!host) return fallback;"),
|
|
147
|
+
"the portal entry persists across collapse (no footer jump); footer fallback only covers portal failure"
|
|
148
|
+
);
|
|
149
|
+
assert.ok(
|
|
150
|
+
clientSource.includes('className: `${nativeCls} mneme-topentry-native`'.replace("${nativeCls} mneme-topentry-native", "${nativeCls} mneme-topentry-native")),
|
|
151
|
+
"the entry must reuse the host New-Session button class for native geometry alignment"
|
|
152
|
+
);
|
|
153
|
+
assert.ok(
|
|
154
|
+
/querySelector\('\[class\*="newSession"\]'\)/.test(clientSource),
|
|
155
|
+
"the native class must be read from the live New-Session button, not hardcoded"
|
|
131
156
|
);
|
|
132
157
|
assert.ok(
|
|
133
158
|
clientSource.includes('"memory.view.label"'),
|
|
134
|
-
"the
|
|
159
|
+
"the sheet aria-label must come from the memory.view.label dictionary key"
|
|
135
160
|
);
|
|
136
161
|
});
|
|
137
162
|
|
|
@@ -199,16 +224,21 @@ test("entity: search grammar offers a graph jump", () => {
|
|
|
199
224
|
);
|
|
200
225
|
});
|
|
201
226
|
|
|
202
|
-
// The explorer is a
|
|
203
|
-
//
|
|
227
|
+
// The explorer is a card-grid / timeline browse with a right-hand detail
|
|
228
|
+
// drawer: types with counts, a month→day time tree (timeline mode), and the
|
|
229
|
+
// drawer rendering the untruncated content.
|
|
204
230
|
test("explorer lays out types, timeline, and full-text detail", () => {
|
|
205
231
|
assert.ok(
|
|
206
232
|
clientSource.includes('className: "mneme-xmain"'),
|
|
207
233
|
"the three-column grid must be present"
|
|
208
234
|
);
|
|
209
235
|
assert.ok(
|
|
210
|
-
clientSource.includes('className: "mneme-
|
|
211
|
-
"the detail
|
|
236
|
+
clientSource.includes('className: "mneme-dcontent"'),
|
|
237
|
+
"the detail drawer must render the full content"
|
|
238
|
+
);
|
|
239
|
+
assert.ok(
|
|
240
|
+
clientSource.includes('className: "mneme-cards"'),
|
|
241
|
+
"the card-grid view must exist alongside the timeline"
|
|
212
242
|
);
|
|
213
243
|
assert.ok(
|
|
214
244
|
/toLocaleDateString\(undefined, \{ year: "numeric", month: "long" \}\)/.test(clientSource),
|
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
import { createStore } from "../../src/store.js";
|
|
6
6
|
|
|
7
7
|
const [, , dbPath, iterations] = process.argv;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
// Windows CI 上 8 进程并发写同一 generation 行时,busy_timeout(5s) 到期仍偶发
|
|
10
|
+
// SQLITE_BUSY("database is locked")。锁是瞬时资源竞争,线性重试即可收敛;
|
|
11
|
+
// incrementGeneration 是单条原子 UPDATE(失败即未执行),重试不会重复计数。
|
|
12
|
+
async function withBusyRetry(fn) {
|
|
13
|
+
for (let attempt = 0; ; attempt++) {
|
|
14
|
+
try {
|
|
15
|
+
return fn();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (!/database is locked|SQLITE_BUSY/i.test(String(err?.message ?? err)) || attempt >= 40) throw err;
|
|
18
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const store = await withBusyRetry(() => createStore(dbPath));
|
|
24
|
+
for (let i = 0; i < Number(iterations); i++) await withBusyRetry(() => store.incrementGeneration());
|
|
10
25
|
store.close();
|
|
@@ -114,7 +114,15 @@ test("peer-C: 多进程并发原子递增——8 进程×10 次 incrementGenerat
|
|
|
114
114
|
store.close();
|
|
115
115
|
}
|
|
116
116
|
} finally {
|
|
117
|
-
|
|
117
|
+
// Windows:8 个 SQLite 子进程退出后 -shm/-wal 句柄延迟释放,删目录偶发 EPERM;
|
|
118
|
+
// rm 带 maxRetries 自动重试(30×100ms 共 3s),仍失败则放行——清理只是收尾,
|
|
119
|
+
// 并发断言正确性由上方 generation 断言保证,残留临时目录交给 OS/CI 回收,
|
|
120
|
+
// 绝不让句柄释放慢把 flaky 标红。
|
|
121
|
+
try {
|
|
122
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 30, retryDelay: 100 });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (!["EPERM", "EBUSY", "EACCES"].includes(err.code)) throw err;
|
|
125
|
+
}
|
|
118
126
|
}
|
|
119
127
|
});
|
|
120
128
|
|
package/test/settings.test.js
CHANGED
|
@@ -123,3 +123,139 @@ test("external api settings persist token and merge partial writes", () => {
|
|
|
123
123
|
assert.deepEqual(settings.getExternalApi(), { enabled: true, port: 9000, host: "0.0.0.0", token: "tok-2" });
|
|
124
124
|
store.close();
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
// --- feature flags ---
|
|
128
|
+
|
|
129
|
+
test("feature flags default to an empty object", () => {
|
|
130
|
+
const { store, settings } = setup();
|
|
131
|
+
assert.deepEqual(settings.getFeatureFlags(), {});
|
|
132
|
+
store.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("setFeatureFlags rejects unknown keys and persists nothing", () => {
|
|
136
|
+
const { store, settings } = setup();
|
|
137
|
+
settings.setFeatureFlags({ autoDream: false });
|
|
138
|
+
assert.throws(() => settings.setFeatureFlags({ noSuchFlag: true }), TypeError);
|
|
139
|
+
assert.throws(() => settings.setFeatureFlags({ noSuchFlag: true }), /noSuchFlag/);
|
|
140
|
+
// 被拒的 patch 不落库,已存的合法值原样保留
|
|
141
|
+
assert.deepEqual(settings.getFeatureFlags(), { autoDream: false });
|
|
142
|
+
store.close();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("setFeatureFlags enforces boolean types and integer ranges", () => {
|
|
146
|
+
const { store, settings } = setup();
|
|
147
|
+
assert.throws(() => settings.setFeatureFlags({ autoInject: "yes" }), /autoInject/);
|
|
148
|
+
assert.throws(() => settings.setFeatureFlags({ distillMaxChars: 500 }), /distillMaxChars/);
|
|
149
|
+
assert.throws(() => settings.setFeatureFlags({ distillRateLimitRetries: 1.5 }), /distillRateLimitRetries/);
|
|
150
|
+
assert.throws(() => settings.setFeatureFlags({ codingBoostFactor: 9 }), /codingBoostFactor/);
|
|
151
|
+
assert.deepEqual(settings.getFeatureFlags(), {}, "failed writes persist nothing");
|
|
152
|
+
store.close();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("setFeatureFlags merges partial patches and persists across instances", () => {
|
|
156
|
+
const { store, settings } = setup();
|
|
157
|
+
settings.setFeatureFlags({ autoDream: false, distillMaxChars: 48000 });
|
|
158
|
+
settings.setFeatureFlags({ rerankEnabled: true });
|
|
159
|
+
// 同一 kv 上的新实例读回合并结果,证明已真正持久化
|
|
160
|
+
const settings2 = createSettings(store.db);
|
|
161
|
+
assert.deepEqual(settings2.getFeatureFlags(), { autoDream: false, distillMaxChars: 48000, rerankEnabled: true });
|
|
162
|
+
store.close();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("getFeatureFlags drops unknown and corrupted keys from the kv", () => {
|
|
166
|
+
const { store, settings } = setup();
|
|
167
|
+
settings.setFeatureFlags({ autoInject: false });
|
|
168
|
+
// 模拟旧版本/手改残留:绕过校验直接写脏 kv
|
|
169
|
+
store.db.prepare("UPDATE user_settings SET value = ? WHERE key = ?").run(
|
|
170
|
+
JSON.stringify({ autoInject: false, legacyFlag: true, autoDream: "yes", distillMaxChars: 999999 }),
|
|
171
|
+
"feature_flags"
|
|
172
|
+
);
|
|
173
|
+
assert.deepEqual(settings.getFeatureFlags(), { autoInject: false });
|
|
174
|
+
// 整个 kv 损坏(非 JSON)→ 读回空对象而不是抛错
|
|
175
|
+
store.db.prepare("UPDATE user_settings SET value = ? WHERE key = ?").run("not-json", "feature_flags");
|
|
176
|
+
assert.deepEqual(settings.getFeatureFlags(), {});
|
|
177
|
+
store.close();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("feature flags accept the new boolean, string, url and enum keys", () => {
|
|
181
|
+
const { store, settings } = setup();
|
|
182
|
+
settings.setFeatureFlags({
|
|
183
|
+
"memoryQualityFilter.enabled": false,
|
|
184
|
+
"llmAudit.enabled": false,
|
|
185
|
+
bm25SearchEnabled: false,
|
|
186
|
+
conflictFreezeEnabled: true,
|
|
187
|
+
trustEpistemicWeighting: true,
|
|
188
|
+
reflectionFailureTracking: false,
|
|
189
|
+
dreamProvider: " siliconflow ",
|
|
190
|
+
dreamModel: "deepseek-chat",
|
|
191
|
+
localEmbedModel: "Xenova/bge-small-zh-v1.5",
|
|
192
|
+
ollamaModel: "nomic-embed-text",
|
|
193
|
+
ollamaBaseUrl: "http://127.0.0.1:11434/",
|
|
194
|
+
embedProvider: "ollama"
|
|
195
|
+
});
|
|
196
|
+
const flags = settings.getFeatureFlags();
|
|
197
|
+
assert.equal(flags["memoryQualityFilter.enabled"], false, "nested keys stored flat");
|
|
198
|
+
assert.equal(flags["llmAudit.enabled"], false);
|
|
199
|
+
assert.equal(flags.bm25SearchEnabled, false);
|
|
200
|
+
assert.equal(flags.conflictFreezeEnabled, true);
|
|
201
|
+
assert.equal(flags.trustEpistemicWeighting, true);
|
|
202
|
+
assert.equal(flags.reflectionFailureTracking, false);
|
|
203
|
+
assert.equal(flags.dreamProvider, "siliconflow", "string values are trimmed");
|
|
204
|
+
assert.equal(flags.dreamModel, "deepseek-chat");
|
|
205
|
+
assert.equal(flags.ollamaBaseUrl, "http://127.0.0.1:11434/");
|
|
206
|
+
assert.equal(flags.embedProvider, "ollama");
|
|
207
|
+
// 同一 kv 上的新实例读回合并结果:点号键按平铺键持久化
|
|
208
|
+
const settings2 = createSettings(store.db);
|
|
209
|
+
assert.equal(settings2.getFeatureFlags()["memoryQualityFilter.enabled"], false);
|
|
210
|
+
assert.equal(settings2.getFeatureFlags().embedProvider, "ollama");
|
|
211
|
+
store.close();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("nested feature flag keys are validated as booleans and rejected otherwise", () => {
|
|
215
|
+
const { store, settings } = setup();
|
|
216
|
+
assert.throws(() => settings.setFeatureFlags({ "memoryQualityFilter.enabled": "yes" }), /memoryQualityFilter\.enabled/);
|
|
217
|
+
assert.throws(() => settings.setFeatureFlags({ "llmAudit.enabled": 1 }), /llmAudit\.enabled/);
|
|
218
|
+
assert.deepEqual(settings.getFeatureFlags(), {}, "failed writes persist nothing");
|
|
219
|
+
store.close();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("string feature flags: empty is legal, over-length and enum violations rejected", () => {
|
|
223
|
+
const { store, settings } = setup();
|
|
224
|
+
// 空字符串合法 = 跟随主对话模型(面板显示 placeholder)
|
|
225
|
+
settings.setFeatureFlags({ dreamProvider: "", dreamModel: " " });
|
|
226
|
+
assert.equal(settings.getFeatureFlags().dreamProvider, "");
|
|
227
|
+
assert.equal(settings.getFeatureFlags().dreamModel, "", "whitespace-only trims to empty");
|
|
228
|
+
assert.throws(() => settings.setFeatureFlags({ dreamModel: "x".repeat(201) }), /dreamModel/);
|
|
229
|
+
assert.throws(() => settings.setFeatureFlags({ localEmbedModel: 42 }), /localEmbedModel/);
|
|
230
|
+
assert.throws(() => settings.setFeatureFlags({ embedProvider: "bogus" }), /embedProvider/);
|
|
231
|
+
assert.throws(() => settings.setFeatureFlags({ embedProvider: "OpenAI" }), /embedProvider/, "enum is case-sensitive");
|
|
232
|
+
assert.deepEqual(settings.getFeatureFlags(), { dreamProvider: "", dreamModel: "" }, "failed writes persist nothing");
|
|
233
|
+
store.close();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("ollamaBaseUrl accepts empty and http(s) URLs, rejects other protocols", () => {
|
|
237
|
+
const { store, settings } = setup();
|
|
238
|
+
settings.setFeatureFlags({ ollamaBaseUrl: "" });
|
|
239
|
+
assert.equal(settings.getFeatureFlags().ollamaBaseUrl, "", "empty = follow default");
|
|
240
|
+
settings.setFeatureFlags({ ollamaBaseUrl: " https://embed.example.com/v1 " });
|
|
241
|
+
assert.equal(settings.getFeatureFlags().ollamaBaseUrl, "https://embed.example.com/v1");
|
|
242
|
+
assert.throws(() => settings.setFeatureFlags({ ollamaBaseUrl: "ftp://localhost:11434" }), /ollamaBaseUrl/);
|
|
243
|
+
assert.throws(() => settings.setFeatureFlags({ ollamaBaseUrl: "file:///etc/passwd" }), /ollamaBaseUrl/);
|
|
244
|
+
assert.throws(() => settings.setFeatureFlags({ ollamaBaseUrl: "javascript:alert(1)" }), /ollamaBaseUrl/);
|
|
245
|
+
assert.throws(() => settings.setFeatureFlags({ ollamaBaseUrl: "localhost:11434" }), /ollamaBaseUrl/, "protocol is required");
|
|
246
|
+
assert.throws(() => settings.setFeatureFlags({ ollamaBaseUrl: "not a url" }), /ollamaBaseUrl/);
|
|
247
|
+
assert.equal(settings.getFeatureFlags().ollamaBaseUrl, "https://embed.example.com/v1", "rejected writes persist nothing");
|
|
248
|
+
store.close();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("getFeatureFlags drops corrupted string, url and enum keys from the kv", () => {
|
|
252
|
+
const { store, settings } = setup();
|
|
253
|
+
settings.setFeatureFlags({ dreamModel: "m", ollamaBaseUrl: "http://localhost:11434", embedProvider: "local" });
|
|
254
|
+
// 模拟旧版本/手改残留:字符串键混入非字符串、URL 键混入非法协议、枚举越界
|
|
255
|
+
store.db.prepare("UPDATE user_settings SET value = ? WHERE key = ?").run(
|
|
256
|
+
JSON.stringify({ dreamModel: 42, ollamaBaseUrl: "ftp://bad", embedProvider: "bogus", autoInject: false }),
|
|
257
|
+
"feature_flags"
|
|
258
|
+
);
|
|
259
|
+
assert.deepEqual(settings.getFeatureFlags(), { autoInject: false });
|
|
260
|
+
store.close();
|
|
261
|
+
});
|
package/test/summarize.test.js
CHANGED
|
@@ -293,6 +293,34 @@ test("distills full transcript (tool calls, results, code output) with atomic-me
|
|
|
293
293
|
assert.ok(calls[0].messages[0].content[0].text.includes("原子记忆"));
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
test("distill excludes private assistant reasoning blocks from the transcript", async () => {
|
|
297
|
+
const { events, calls } = setup();
|
|
298
|
+
const handler = events.find((e) => e.name === "session/event").fn;
|
|
299
|
+
const session = {
|
|
300
|
+
id: "s12",
|
|
301
|
+
requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
|
|
302
|
+
events: [
|
|
303
|
+
userMessage("解释一下这段代码"),
|
|
304
|
+
{
|
|
305
|
+
type: "assistant/message",
|
|
306
|
+
data: {
|
|
307
|
+
message: {
|
|
308
|
+
content: [
|
|
309
|
+
{ type: "reasoning", text: "私有推理:内部权衡过程不该进记忆" },
|
|
310
|
+
{ type: "text", text: "这段代码有死循环,第 3 行 while 条件恒真。" }
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{ seq: 3, type: "turn/end" }
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
await handler(session, { seq: 3, type: "turn/end" });
|
|
319
|
+
const transcript = JSON.stringify(calls[0].messages);
|
|
320
|
+
assert.ok(transcript.includes("有死循环,第 3 行"), "public assistant text still distills");
|
|
321
|
+
assert.ok(!transcript.includes("私有推理"), "private reasoning must never enter the distill context");
|
|
322
|
+
});
|
|
323
|
+
|
|
296
324
|
test("codingRetrospect stores coding memory types in the coding memory type set", async () => {
|
|
297
325
|
const { events, store, calls } = setup({ codingRetrospect: true }, {
|
|
298
326
|
stream() {
|