dsh-lite-memory 0.18.42 → 0.18.44

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.
@@ -1,400 +0,0 @@
1
- import assert from 'node:assert/strict';
2
-
3
- let seen = null;
4
- let plugin = null;
5
- const makeNode = (type, props, ...children) => ({ type, props: props || {}, children: children.flat(Infinity) });
6
- const fakeReact = { createElement: makeNode, useRef: (value) => ({ current: value }), useState: (value) => [value, () => {}], useEffect: () => {}, useMemo: (fn) => fn() };
7
- globalThis.window = { __ModuleLoader__: { load: (definition) => { plugin = definition.factory((name) => (name === 'react' ? fakeReact : null)); } } };
8
- globalThis.__LITE_MEMORY_TEST__ = (helpers) => { seen = helpers; };
9
- await import('../client.js');
10
- assert.ok(seen, 'client test seam missing');
11
-
12
- // 会话头的项目开关:注册到宿主认的位置,状态只能问服务端。
13
- const registrations = [];
14
- const slots = {
15
- inject: (_name, callback) => callback(),
16
- register: (definition) => { registrations.push(definition); return () => {}; },
17
- };
18
- plugin.apply({ slots, sessions: { binding: () => undefined }, uiConversation: {} });
19
- const evolution = registrations.find((item) => item.id === 'lite-memory:project-evolution');
20
- assert.ok(evolution, '项目开关没有注册');
21
- assert.equal(evolution.name, 'conversation.session.header.utilities');
22
- assert.equal(evolution.order, 30);
23
-
24
- // 记忆面板占整框浮层,不挤在会话列里。
25
- const panel = registrations.find((item) => item.id === 'lite-memory:panel');
26
- assert.ok(panel, '记忆面板没有注册');
27
- assert.equal(panel.name, 'shell.overlay');
28
- assert.equal(panel.order, 30);
29
-
30
- const EMPTY_CHILDREN = [];
31
- const { metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, MemoryPalace, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf, TurnMemory, refMetaOf, explainKeyOf, explainModeOf, setExplainMode, recallLineOf, agoLabel, checkupOf, refSectionRowsOf, refDetailTitleOf, versionLine, titleCountsOf, rowKeyOf, rowKeepFlags, conflictVisibleRows, conflictFactsOf } = seen;
32
-
33
- // 图谱只按真实数据画:共享标签成枢纽,显式关联成边,位置由 id 决定。
34
- const graph = graphOf([
35
- { id: 'mem_aaaaaaaa', title: 'A', tags: ['发布'], topicKey: '上线', experimental: false, links: [{ id: 'mem_bbbbbbbb' }] },
36
- { id: 'mem_bbbbbbbb', title: 'B', tags: ['发布'], topicKey: '上线', experimental: true, links: [] },
37
- { id: 'mem_cccccccc', title: 'C', tags: ['环境'], topicKey: '', experimental: false, links: [] },
38
- ]);
39
- assert.equal(graph.nodes.filter((node) => node.kind === 'memory').length, 3);
40
- assert.equal(graph.nodes.some((node) => node.id === 'tag:发布'), true);
41
- assert.equal(graph.nodes.some((node) => node.id === 'tag:环境'), false);
42
- assert.equal(graph.nodes.some((node) => node.id === 'topic:上线'), false);
43
- assert.equal(graph.edges.filter((edge) => edge.type === 'tag').length, 2);
44
- assert.equal(graph.edges.filter((edge) => edge.type === 'link').length, 1);
45
- assert.deepEqual(graphOf([{ id: 'mem_aaaaaaaa', title: 'A' }]).nodes[0].x, graphOf([{ id: 'mem_aaaaaaaa', title: 'A' }]).nodes[0].x);
46
- const pair = graphOf([{ id: 'mem_aaaaaaaa', title: 'A' }, { id: 'mem_bbbbbbbb', title: 'B' }]);
47
- assert.notEqual(pair.nodes[0].x, pair.nodes[1].x);
48
- assert.notEqual(pair.nodes[0].y, pair.nodes[1].y);
49
-
50
- // 共享标签是组:枢纽点比成员大,成员围着它,孤立记忆留在外圈;被更强组吸收的次要标签不另画。
51
- const hubNode = graph.nodes.find((node) => node.id === 'tag:发布');
52
- const memberA = graph.nodes.find((node) => node.id === 'mem_aaaaaaaa');
53
- const memberC = graph.nodes.find((node) => node.id === 'mem_cccccccc');
54
- assert.ok(hubNode && memberA && memberC);
55
- assert.ok(hubNode.radius > memberA.radius + 4, '共享标签枢纽应比成员大');
56
- assert.ok(Math.hypot(memberA.x - hubNode.x, memberA.y - hubNode.y) < 90, '成员应靠近所属枢纽');
57
- assert.ok(Math.hypot(memberC.x - hubNode.x, memberC.y - hubNode.y) > 90, '无共享标签的记忆应留在外圈');
58
-
59
- // 一条记忆只连自己所属组的枢纽:别的共享标签不画跨组虚线。
60
- const overlap = graphOf([
61
- { id: 'mem_dddddddd', title: 'D', tags: ['X', 'Y'] },
62
- { id: 'mem_eeeeeeee', title: 'E', tags: ['X', 'Y'] },
63
- { id: 'mem_ffffffff', title: 'F', tags: ['X'] },
64
- { id: 'mem_gggggggg', title: 'G', tags: ['Y'] },
65
- { id: 'mem_hhhhhhhh', title: 'H', tags: ['Y'] },
66
- { id: 'mem_iiiiiiii', title: 'I', tags: ['X'] },
67
- ]);
68
- const overlapTags = overlap.edges.filter((edge) => edge.type === 'tag').map((edge) => `${edge.from}>${edge.to}`);
69
- assert.equal(overlapTags.length, 6);
70
- assert.equal(overlapTags.includes('mem_dddddddd>tag:Y'), false);
71
- assert.equal(overlapTags.includes('mem_eeeeeeee>tag:Y'), false);
72
-
73
- // 经验图谱只按共享的适用条件相连,不画没有依据的关系。
74
- const rules = [
75
- { id: 'ev_aaaaaaaa', title: 'A', state: 'active', conditions: [{ key: 'seriesId', value: '口播' }] },
76
- { id: 'ev_bbbbbbbb', title: 'B', state: 'trial', conditions: [{ key: 'seriesId', value: '口播' }, { key: 'phase', value: '初稿' }] },
77
- { id: 'ev_cccccccc', title: 'C', state: 'pending', conditions: [{ key: 'phase', value: '初稿' }] },
78
- ];
79
- const ruleGraph = ruleGraphOf(rules);
80
- assert.equal(ruleGraph.nodes.filter((node) => node.kind === 'rule').length, 3);
81
- assert.equal(ruleGraph.nodes.some((node) => node.id === 'condition:seriesId:口播'), true);
82
- assert.equal(ruleGraph.nodes.some((node) => node.id === 'condition:phase:初稿'), true);
83
- assert.equal(ruleGraph.edges.length, 4);
84
- assert.equal(ruleGraph.edges.every((edge) => edge.type === 'condition'), true);
85
- assert.equal(ruleGraph.nodes.some((node) => node.kind === 'tag' || node.kind === 'topic'), false);
86
- assert.deepEqual(ruleGraphOf(rules).nodes.map((node) => [node.id, node.x, node.y]), ruleGraph.nodes.map((node) => [node.id, node.x, node.y]));
87
- assert.equal(ruleGraphOf([{ id: 'ev_aaaaaaaa', title: 'A', state: 'active', conditions: [] }]).edges.length, 0);
88
-
89
- const settled = (meta, extra = {}) => ({ kind: 'tool-result', meta, isError: false, content: [], ...extra });
90
-
91
- // 元数据只从落定结果里取,错误块和旧块一律不认。
92
- assert.equal(metaOf(settled({ kind: 'memory-read', id: 'mem_aaaaaaaa' })).kind, 'memory-read');
93
- assert.equal(metaOf({ phase: 'preparing' }), null);
94
- assert.equal(metaOf(settled({ kind: 'memory-read', id: 'mem_aaaaaaaa' }, { isError: true })), null);
95
- assert.equal(metaOf(settled(null)), null);
96
- assert.equal(metaOf(settled([{ kind: 'memory-read' }])), null);
97
-
98
- // 保存只在真的改了时进引用;正文保存不冒充引用。
99
- const save = citationOf({ kind: 'memory-save', title: 'A', scope: 'project', revision: 3, changed: false, mode: 'unchanged' }, 'memory-save');
100
- assert.equal(save, null);
101
- const changed = citationOf({ kind: 'memory-save', title: 'A', scope: 'global', revision: 2, changed: true, mode: 'updated' }, 'memory-save');
102
- assert.equal(changed.exposure, 'update');
103
- assert.equal(changed.scope, 'global');
104
-
105
- // 没有真改动、缺版本、缺工作目录的写入行不产生撤销入口。
106
- assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 3, changed: false }, '/tmp/p'), null);
107
- assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 3 }, '/tmp/p'), null);
108
- assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 3, changed: true }, ''), null);
109
- assert.equal(undoRefOf({ kind: 'memory-save', revision: 2, changed: true }, '/tmp/p'), null);
110
- assert.equal(undoRefOf({ kind: 'memory-save', title: 'A', revision: 2, changed: true }, '/tmp/p').revision, 2);
111
-
112
- // 搜索的多条引用按 refs 展开;删除/归档状态原样保留。
113
- const refs = citationsOfMeta({ kind: 'memory-search', refs: [
114
- { title: 'A', scope: 'project', revision: 1, status: 'ok' },
115
- { title: 'B', scope: 'global', revision: 4, status: 'archived' },
116
- { title: '', scope: 'global', revision: 1, status: 'ok' },
117
- ] });
118
- assert.equal(refs.length, 2);
119
- assert.equal(refs[1].status, 'archived');
120
- assert.equal(refs[0].exposure, 'reference');
121
-
122
- // 一轮里 PTC 子结果也算;同 id 同用途只留一条,位置不变、内容取最新。
123
- const nodes = [
124
- { root: settled({ kind: 'memory-search', refs: [
125
- { title: 'A', scope: 'project', revision: 1, status: 'ok' },
126
- ] }) },
127
- { root: settled({ kind: 'memory-read', title: 'A', scope: 'project', revision: 2, status: 'ok' }, { subCalls: [settled({ kind: 'memory-read', title: 'A', scope: 'project', revision: 2, status: 'ok' })] }) },
128
- { root: settled({ kind: 'memory-read', title: 'B', scope: 'global', revision: 1, status: 'ok' }) },
129
- ];
130
- const turn = dedupe(citationsOfTurn(nodes));
131
- assert.equal(turn.length, 2);
132
- assert.equal(turn[0].title, 'A');
133
- assert.equal(turn[0].revision, 2);
134
- assert.equal(turn[1].title, 'B');
135
- assert.equal('id' in turn[0], false);
136
-
137
- // 旧行没有元数据:什么也不产出,绝不从文案猜 id。
138
- assert.deepEqual(citationsOfTurn([{ root: null }, { root: { kind: 'tool-result', content: [{ type: 'text', text: 'memory · saved "A" mem_aaaaaaaa' }] } }]), []);
139
-
140
- // 处理状态只认服务端枚举;中文 reason 不参与猜测。
141
- assert.equal(processingStatusOf({ processingStatus: 'applied' }), 'applied');
142
- assert.equal(processingStatusOf({ outcome: { status: 'superseded' } }), 'superseded');
143
- assert.equal(processingStatusOf({ status: 'failed' }), 'failed');
144
- assert.equal(processingStatusOf({ status: 'ok', reason: '处理失败' }), '');
145
- assert.equal(processingStatusOf({ reason: '已应用' }), '');
146
-
147
- // 当前正文与历史版本分开:历史行只给 old → new,不拼进当前正文。
148
- const memoryPayload = {
149
- currentBody: '新的正文',
150
- versions: [
151
- { revision: 1, body: '旧的正文', at: '2026-01-01' },
152
- { revision: 2, body: '中间正文', at: '2026-02-01' },
153
- ],
154
- };
155
- assert.equal(currentBodyOf(memoryPayload), '新的正文');
156
- const diffs = historyDiffsOf(memoryPayload);
157
- assert.equal(diffs.length, 2);
158
- assert.equal(diffs[0].old, '旧的正文');
159
- assert.equal(diffs[0].next, '中间正文');
160
- assert.equal(diffs[1].next, '新的正文');
161
- assert.equal(currentBodyOf({ record: { current: { body: '记录正文' }, body: '旧字段' } }), '记录正文');
162
- assert.deepEqual(historyDiffsOf({ body: '单版本' }), []);
163
-
164
- // 图谱截断时保留真实总数,60 是绘制上限而不是总数。
165
- const many = Array.from({ length: 65 }, (_, index) => ({ id: `mem_${String(index).padStart(8, '0')}`, title: `T${index}` }));
166
- const big = graphOf(many);
167
- assert.equal(big.shown, 60);
168
- assert.equal(big.total, 65);
169
- assert.equal(big.nodes.filter((node) => node.kind === 'memory').length, 60);
170
- assert.equal(ruleGraphOf(Array.from({ length: 65 }, (_, index) => ({ id: `ev_${String(index).padStart(8, '0')}`, title: `R${index}`, conditions: [] }))).total, 65);
171
-
172
- // 显示设置只决定画不画;按项目 key 分开存,默认都关。
173
- const memoryStore = new Map();
174
- globalThis.localStorage = { getItem: (key) => (memoryStore.has(key) ? memoryStore.get(key) : null), setItem: (key, value) => memoryStore.set(key, String(value)) };
175
- assert.equal(displayKeyOf('project:/a'), 'lite-memory:display:project:/a');
176
- assert.notEqual(displayKeyOf('project:/a'), displayKeyOf('project:/b'));
177
- assert.deepEqual(readDisplaySettings('project:/a'), { graph: false, palace: false });
178
- writeDisplaySettings('project:/a', { graph: true, palace: false });
179
- assert.deepEqual(readDisplaySettings('project:/a'), { graph: true, palace: false });
180
- assert.deepEqual(readDisplaySettings('project:/b'), { graph: false, palace: false });
181
- memoryStore.set(displayKeyOf('project:/c'), '{oops');
182
- assert.deepEqual(readDisplaySettings('project:/c'), { graph: false, palace: false });
183
- assert.equal(graphViewOf(false, 'graph'), 'list');
184
- assert.equal(graphViewOf(true, 'graph'), 'graph');
185
- assert.equal(graphViewOf(undefined, 'graph'), 'list');
186
-
187
- // 点标签/主题按精确值筛,不把「发布」发散成「发布会」。
188
- const tagged = [
189
- { id: 'mem_aaaaaaaa', title: '发布记录', tags: ['发布'], topicKey: '上线' },
190
- { id: 'mem_bbbbbbbb', title: '发布会记录', tags: ['发布会'], topicKey: '上线复盘' },
191
- ];
192
- assert.deepEqual(filterRecords(tagged, '', { kind: 'tag', value: '发布' }).map((row) => row.id), ['mem_aaaaaaaa']);
193
- assert.deepEqual(filterRecords(tagged, '', { kind: 'topic', value: '上线' }).map((row) => row.id), ['mem_aaaaaaaa']);
194
- assert.deepEqual(filterRecords(tagged, '发布会', null).map((row) => row.id), ['mem_bbbbbbbb']);
195
-
196
- // 60 条投影外也要保住当前选中项,不能点开就消失。
197
- const long = Array.from({ length: 65 }, (_, index) => ({ id: `mem_${String(index).padStart(8, '0')}`, title: `T${index}` }));
198
- const kept = projectionOf(long, long[64].id);
199
- assert.equal(kept.length, 60);
200
- assert.equal(kept[59].id, long[64].id);
201
- assert.equal(graphOf(long, long[64].id).nodes.some((node) => node.id === long[64].id), true);
202
- assert.equal(graphOf(long).nodes.some((node) => node.id === long[64].id), false);
203
-
204
- // 失败分当前和过去:只有最近一次失败才提示现有经验没更新。
205
- assert.equal(failureStateOf({ economy: { recent: [{ status: 'failed' }, { status: 'applied' }] } }), 'current');
206
- assert.equal(failureStateOf({ economy: { recent: [{ status: 'applied' }, { status: 'failed' }] } }), 'past');
207
- assert.equal(failureStateOf({ economy: { recent: [{ status: 'applied' }] } }), '');
208
- assert.equal(failureStateOf(null), '');
209
-
210
- // 学习依据读真实摘录与样例元数据,不再拿「样例」占位。
211
- assert.equal(evidenceTextOf({ sourceExcerpt: '用户说别用渐变' }), '用户说别用渐变');
212
- assert.equal(evidenceTextOf({ excerpt: '原文' }), '原文');
213
- assert.equal(evidenceTextOf({ note: '备注' }), '备注');
214
- assert.equal(evidenceTextOf({ feedbackType: 'negative' }), '');
215
- assert.equal(evidenceMetaOf({ artifactVersion: 2, dimension: '排版', verified: true, at: '2026-03-04T00:00:00Z' }), '第 2 版 · 排版 · 已核对 · 2026-03-04');
216
- assert.equal(evidenceMetaOf({ verified: false }), '未核对');
217
-
218
- // 显示设置默认关闭:面板首帧只出列表,不挂载图谱组件。
219
- const nodeTypes = (node, out = []) => {
220
- if (!node || typeof node !== 'object') return out;
221
- if (node.type) out.push(node.type);
222
- for (const child of node.children || EMPTY_CHILDREN) nodeTypes(child, out);
223
- return out;
224
- };
225
- const firstPaint = MemoryPanel({ sessionId: 'demo', onClose: () => {} });
226
- assert.ok(firstPaint, '面板首帧应渲染');
227
- assert.equal(nodeTypes(firstPaint).includes(MemoryGraph), false);
228
- assert.equal(JSON.stringify(nodeTypes(firstPaint)).includes('MemoryGraph'), false);
229
- assert.equal(nodeTypes(firstPaint).includes(MemoryPalace), false);
230
- assert.equal(JSON.stringify(nodeTypes(firstPaint)).includes('MemoryPalace'), false);
231
-
232
-
233
- // 记忆宫殿:固定四间,空房保留位置,未知类型有归处。
234
- const palaceRecord = (id, kind, title = id) => ({ id, kind, title });
235
- const palaceRooms = palaceRoomsOf([
236
- palaceRecord('mem_1', 'note'), palaceRecord('mem_2', 'pointer'), palaceRecord('mem_3', 'procedure'),
237
- palaceRecord('mem_4', 'preference'), palaceRecord('mem_5', 'convention'), palaceRecord('mem_6', 'pitfall'),
238
- palaceRecord('mem_7', 'credential_hint'), palaceRecord('mem_8', 'unknown_kind'),
239
- ]);
240
- assert.deepEqual(palaceRooms.map((room) => room.name), ['习惯与规范', '方法与经验', '资料与入口', '其他记录']);
241
- assert.deepEqual(palaceRooms.map((room) => room.total), [2, 2, 2, 2]);
242
- assert.equal(palaceRoomIdOf('unknown_kind'), 'notes');
243
- assert.deepEqual(palaceRoomsOf([palaceRecord('mem_9', 'note')]).map((room) => room.total), [0, 0, 0, 1]);
244
- assert.equal(palaceRoomsOf([]).length, 4);
245
- // 每页 24 条;页码夹在有效范围内。
246
- const palaceMany = Array.from({ length: 25 }, (_, index) => palaceRecord(`mem_${index}`, 'preference', `P${index}`));
247
- assert.equal(palaceRoomsOf(palaceMany)[0].pages, 2);
248
- assert.equal(palaceRoomPageOf(palaceMany, 'habits', 1).items.length, 24);
249
- assert.equal(palaceRoomPageOf(palaceMany, 'habits', 2).items.length, 1);
250
- assert.equal(palaceRoomPageOf(palaceMany, 'habits', 99).page, 2);
251
- assert.equal(palaceRoomPageOf(palaceMany, 'habits', 0).page, 1);
252
- assert.deepEqual(palacePageWindow(1, 3), [1, 2, 3]);
253
- assert.deepEqual(palacePageWindow(5, 12), [2, 3, 4, 5, 6, 7, 8]);
254
- assert.equal(palacePageWindow(12, 12).includes(12), true);
255
- // 同一时刻只展开一间,再点收起。
256
- assert.equal(palaceToggleOf('', 'methods'), 'methods');
257
- assert.equal(palaceToggleOf('methods', 'methods'), '');
258
- assert.equal(palaceToggleOf('methods', 'habits'), 'habits');
259
- // 关闭时回列表;减少动态时 0ms,否则 180ms。
260
- assert.equal(viewModeOf({ graph: true, palace: false }, 'palace'), 'list');
261
- assert.equal(viewModeOf({ graph: false, palace: true }, 'palace'), 'palace');
262
- assert.equal(palaceMotionOf(true).duration, 0);
263
- assert.equal(palaceMotionOf(false).duration, 180);
264
- // 展品点开只把 id 交给现有详情入口,不另造详情。
265
- let palacePicked = '';
266
- const palaceTree = MemoryPalace({
267
- records: [palaceRecord('mem_a', 'preference', 'A')],
268
- selectedId: '', onPick: (id) => { palacePicked = id; }, emptyText: '这个项目还没有记忆',
269
- });
270
- const expandNodes = (node, out = []) => {
271
- if (!node || typeof node !== 'object') return out;
272
- if (typeof node.type === 'function') return expandNodes(node.type(node.props), out);
273
- out.push(node);
274
- for (const child of node.children || []) expandNodes(child, out);
275
- return out;
276
- };
277
- const palaceExhibit = expandNodes(palaceTree).find((node) => node.type === 'button' && node.props.title === 'A');
278
- assert.ok(palaceExhibit, '展品应是原生按钮');
279
- palaceExhibit.props.onClick();
280
- assert.equal(palacePicked, 'mem_a');
281
- const palaceEmpty = MemoryPalace({ records: [], selectedId: '', onPick: () => {}, emptyText: '这个项目还没有记忆' });
282
- assert.equal(expandNodes(palaceEmpty).some((node) => node.children.includes('这个项目还没有记忆')), true);
283
-
284
- // 引用行只标层与来源:类型留在数据里给检索用,不上会话界面。
285
- const typedSearch = citationOf({ title: 'A', scope: 'global', revision: 1, status: 'ok', kind: 'preference' }, 'memory-search');
286
- assert.equal(typedSearch.entryKind, 'preference');
287
- assert.equal(refMetaOf(typedSearch), '[全局]');
288
- assert.equal(citationOf({ kind: 'memory-read', title: 'P', recordKind: 'procedure', scope: 'project', revision: 2 }, 'memory-read').entryKind, 'procedure');
289
- assert.equal(refMetaOf({ scope: 'project', origin: 'user_explicit' }), '[项目]');
290
- assert.equal(refMetaOf({ scope: 'global', origin: 'project_evolution' }), '[全局][自进化]');
291
- /** 模型整理稿只多一个信任标记,层级仍按落盘作用域。 */
292
- assert.equal(refMetaOf({ scope: 'project', origin: 'model_drafted' }), '[项目][模型]');
293
- assert.equal(citationOf({ kind: 'memory-save', title: 'M', scope: 'global', revision: 1, origin: 'model_drafted', changed: true }, 'memory-save').origin, 'model_drafted');
294
- /** 更新距今照旧,只是不再拼类型与「你记的」。 */
295
- const aged = citationOf({ kind: 'preference', title: 'C', scope: 'global', revision: 1, updated: new Date(Date.now() - 3 * 86_400_000).toISOString() }, 'memory-search');
296
- assert.equal(aged.entryKind, 'preference');
297
- assert.equal(refMetaOf(aged), '[全局] · 3 天前更新');
298
- assert.equal(agoLabel(''), '');
299
- assert.equal(agoLabel(new Date(Date.now() - 40 * 86_400_000).toISOString()), '');
300
-
301
- // 会话里的正文跟着被点的那条走:点 B,正文夹在 B 与 C 之间,不甩到列表末尾。
302
- const inlineRefs = [
303
- { key: 'a', revision: 1, title: 'A' },
304
- { key: 'b', revision: 2, title: 'B' },
305
- { key: 'c', revision: 3, title: 'C' },
306
- ];
307
- const rowOfRef = (item) => item.key;
308
- const detailOfRef = () => 'detail';
309
- assert.deepEqual(refSectionRowsOf(inlineRefs, { key: 'b', ref: { revision: 2 } }, rowOfRef, detailOfRef), ['a', 'b', 'detail', 'c']);
310
- assert.deepEqual(refSectionRowsOf(inlineRefs, { key: 'a', ref: { revision: 1 } }, rowOfRef, detailOfRef), ['a', 'detail', 'b', 'c']);
311
- assert.deepEqual(refSectionRowsOf(inlineRefs, null, rowOfRef, detailOfRef), ['a', 'b', 'c']);
312
- // 抬头的永远是记忆标题;项目名只留在下一行来源里,不再单独顶一行被当成标题。
313
- assert.equal(refDetailTitleOf({ title: 'DSH', ref: { title: '视频预览插件重启后仍缓存旧代码' } }), '视频预览插件重启后仍缓存旧代码');
314
- assert.equal(refDetailTitleOf({ ref: { title: '标题' } }), '标题');
315
- assert.equal(versionLine({ scope: 'project', scopeLabel: 'DSH', status: 'active' }), '项目 · DSH');
316
- assert.equal(versionLine({ scope: 'global', status: 'active' }), '全局');
317
-
318
- // 解释模式按会话存在本机,缺省关闭;关着零命中不出提示,打开后才说原因。
319
- assert.equal(explainModeOf('s1'), false);
320
- const quietRefs = [];
321
- Object.defineProperty(quietRefs, 'noTriggerReason', { value: '当前任务不适用', enumerable: false, configurable: true });
322
- assert.equal(TurnMemory({ sessionId: 's1', turn: '7', useRefs: () => quietRefs }), null);
323
- setExplainMode('s1', true);
324
- assert.equal(explainModeOf('s1'), true);
325
- assert.equal(explainModeOf('s2'), false);
326
- const told = TurnMemory({ sessionId: 's1', turn: '7', useRefs: () => quietRefs });
327
- assert.ok(told);
328
- assert.equal(JSON.stringify(told).includes('当前任务不适用'), true);
329
- assert.equal(explainKeyOf('s1').startsWith('lite-memory:explain:'), true);
330
-
331
-
332
- const checkupRows = [
333
- { id: 'mem_c0000001', title: '同一件事', when: '', lastUsed: '' },
334
- { id: 'mem_c0000002', title: '同一件事', when: '当 A 时', lastUsed: '2026-01-01T00:00:00.000Z' },
335
- { id: 'mem_c0000003', title: '另一件', when: '当 B 时', lastUsed: '2026-01-01T00:00:00.000Z' },
336
- ];
337
- const checkupScopes = [{ key: 'p', kind: 'project', entries: 3 }, { key: 'global', kind: 'global', entries: 213 }];
338
- assert.deepEqual(checkupOf(checkupRows, checkupScopes, false), { duplicate: 1, unread: 1, globalEntries: 213, globalMissingWhen: null });
339
- assert.deepEqual(checkupOf(checkupRows, checkupScopes, true), { duplicate: 1, unread: 1, globalEntries: 213, globalMissingWhen: 1 });
340
- assert.deepEqual(filterRecords(checkupRows, '', { kind: 'duplicate' }).map((row) => row.id), ['mem_c0000001', 'mem_c0000002']);
341
- assert.deepEqual(filterRecords(checkupRows, '', { kind: 'unread' }).map((row) => row.id), ['mem_c0000001']);
342
- assert.deepEqual(filterRecords([{ id: 'mem_u1', title: '新写的', lastUsed: '2026-01-01T00:00:00.000Z', used: false }], '', { kind: 'unread' }).map((row) => row.id), ['mem_u1'], '有创建时间回退值但没真用过,仍算从未读过');
343
- assert.equal(checkupOf([{ id: 'mem_u2', title: '用过的', lastUsed: '2026-01-01T00:00:00.000Z', used: true }], [], false).unread, 0);
344
- assert.deepEqual(filterRecords(checkupRows, '', { kind: 'noWhen' }).map((row) => row.id), ['mem_c0000001']);
345
- assert.deepEqual([...titleCountsOf(checkupRows).values()], [2, 1]);
346
- assert.equal(checkupOf([], [], false).globalEntries, null);
347
- // 两条一模一样的拦截结果在同一轮里只展示一行:拦截结果没有 id,只能按曝光类型 + 文案 + 正文认。
348
- const blockedBlock = (text) => ({ kind: 'tool-result', callId: 'call-a', content: [{ type: 'text', text }], subCalls: [] });
349
- const blockedText = 'memory · 这条记忆的适用条件还没确认,先说明当前任务再读';
350
- assert.equal(blockedText.includes('mem_'), false);
351
- const blockedKeys = [rowKeyOf('memory_read', blockedBlock(blockedText)), rowKeyOf('memory_read', blockedBlock(blockedText))];
352
- assert.equal(blockedKeys[0].length > 0, true);
353
- assert.deepEqual(rowKeepFlags(blockedKeys), [true, false]);
354
- assert.equal(blockedKeys.filter((_key, at) => rowKeepFlags(blockedKeys)[at]).length, 1);
355
- assert.deepEqual(rowKeepFlags([rowKeyOf('memory_search', blockedBlock(blockedText)), rowKeyOf('memory_read', blockedBlock(blockedText))]), [true, true]);
356
- assert.deepEqual(rowKeepFlags([rowKeyOf('memory_read', blockedBlock('memory · stopped'))]), [true]);
357
- // 面板要能回答「为什么这次没提醒我」:只给结论和条数,不露原因码。
358
- assert.equal(recallLineOf(null), '');
359
- assert.equal(recallLineOf({ reason: 'hit', cards: 1, matched: 1 }), '上次自动提醒 1 条');
360
- assert.equal(recallLineOf({ reason: 'hit', cards: 1, matched: 1, at: new Date(Date.now() - 120000).toISOString() }), '上次自动提醒 1 条 · 2 分钟前');
361
- assert.equal(recallLineOf({ reason: 'no_overlap', at: new Date().toISOString() }), '上次没提醒,没有匹配的记忆 · 刚刚');
362
- assert.equal(recallLineOf({ reason: 'budget', cards: 0, matched: 2 }), '上次没提醒,2 条相关记忆,本轮自动提醒已用满');
363
- assert.equal(recallLineOf({ reason: 'needs_context', cards: 0, matched: 1 }), '上次没提醒,1 条相关记忆,适用条件未确认');
364
- assert.equal(recallLineOf({ reason: 'conflict', cards: 0, matched: 2 }), '上次没提醒,2 条相关记忆,存在冲突的结论');
365
- assert.equal(recallLineOf({ reason: 'no_overlap', cards: 0, matched: 0 }), '上次没提醒,没有匹配的记忆');
366
- assert.equal(recallLineOf({ reason: 'expired', cards: 0, matched: 2 }), '上次没提醒,相关内容已经过期');
367
- assert.equal(recallLineOf({ reason: 'memory_off', cards: 0, matched: 0 }), '上次没提醒,记忆已关闭');
368
- assert.equal(recallLineOf({ reason: 'tools_only', cards: 0, matched: 0 }), '上次没提醒,只在你问时检索');
369
- assert.equal(recallLineOf({ reason: 'made_up', cards: 0, matched: 0 }), '');
370
- assert.doesNotMatch(recallLineOf({ reason: 'needs_context', cards: 0, matched: 1 }), /needs_context|reasonCode/);
371
- console.log('client.test.mjs: 22 passed + palace');
372
-
373
-
374
-
375
- // 待裁决面板:先露两条,查看全部后不截断;展开区给全层级、条件、范围、触发词与完整正文。
376
- const pendingRows = [
377
- { id: 'cf_1', scope: 'global', title: '一', draft: { body: '正文一' } },
378
- { id: 'cf_2', scope: 'project', title: '二', draft: { body: '正文二' } },
379
- { id: 'cf_3', scope: 'project', title: '三', draft: { body: '正文三' } },
380
- ];
381
- assert.deepEqual(conflictVisibleRows(pendingRows, false).map((row) => row.id), ['cf_1', 'cf_2']);
382
- assert.deepEqual(conflictVisibleRows(pendingRows, true).map((row) => row.id), ['cf_1', 'cf_2', 'cf_3']);
383
- const facts = conflictFactsOf({
384
- scope: 'global',
385
- candidates: [{ title: '旧结论' }, { title: '另一条' }],
386
- draft: {
387
- title: '发布窗口', summary: '摘要', body: '周四晚上再发版。', when: '发版前',
388
- conditions: [{ key: 'phase', value: 'release' }], triggers: ['发版', '发布'],
389
- selection: { mode: 'complete', artifactKey: 'release' },
390
- },
391
- });
392
- assert.equal(facts.scope, '总记忆');
393
- assert.equal(facts.when, '发版前');
394
- assert.equal(facts.conditions, 'phase=release');
395
- assert.equal(facts.selection, '清单完整 · release');
396
- assert.equal(facts.triggers, '发版、发布');
397
- assert.equal(facts.near, '旧结论、另一条');
398
- assert.equal(facts.body, '周四晚上再发版。');
399
- assert.equal(conflictFactsOf({ scope: 'project', draft: { selection: { mode: 'examples', items: ['a.md', 'b.md'] } } }).selection, '举例:a.md、b.md');
400
- assert.equal(conflictFactsOf({ draft: { body: '' } }).body, '(没有正文)');