dsh-lite-memory 0.11.1 → 0.16.2

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.
@@ -213,3 +213,314 @@ test('R18 自由措辞不能把未注册栏目变成已绑定', () => {
213
213
  assert.equal(routed.anchor.status, 'unbound');
214
214
  assert.equal(routed.anchor.seriesId, undefined);
215
215
  });
216
+
217
+ test('R12 无切换词的新任务撤销旧作品绑定,不继承栏目和规则', () => {
218
+ const sessionId = 'sess-r12';
219
+ let tasks = anchor.emptyTasks();
220
+ const first = anchor.routeTask(tasks, {
221
+ sessionId,
222
+ batchKey: 'rb_first',
223
+ messageIds: ['m1'],
224
+ query: '帮我把这期口播视频的开头改一下',
225
+ selection: { seriesId: '口播', taskKind: '口播视频' },
226
+ now: 1000,
227
+ });
228
+ assert.equal(first.anchor.status, 'proposed');
229
+ const bound = anchor.applySelection(first.tasks, { sessionId, selection: { seriesId: '口播' }, batchKey: 'rb_first', messageIds: ['m1'], now: 1500 });
230
+ assert.equal(bound.anchor.status, 'proposed');
231
+ const active = bound.tasks.sessions[sessionId];
232
+ active.status = 'active';
233
+ tasks = bound.tasks;
234
+ const next = anchor.routeTask(tasks, {
235
+ sessionId,
236
+ batchKey: 'rb_next',
237
+ messageIds: ['m2'],
238
+ query: '帮我排查 PostgreSQL 连接超时',
239
+ registeredSeries: ['口播'],
240
+ now: 2000,
241
+ });
242
+ assert.equal(next.anchor.status, 'unbound');
243
+ assert.equal(next.decision.kind, 'unbound');
244
+ assert.equal(next.decision.reason, 'new_task');
245
+ assert.ok(next.episode.previousEpisodeId);
246
+
247
+ /** 新任务已撤销旧绑定:旧栏目规则不能再自动进入这一步。 */
248
+ const read = anchor.decideRead({
249
+ enabled: true,
250
+ identityConfirmed: true,
251
+ anchor: next.anchor,
252
+ ranges: [{ seriesId: '口播', taskKind: '口播视频', reason: '旧任务' }],
253
+ });
254
+ assert.equal(read.kind, 'index');
255
+ assert.equal(read.rules, undefined);
256
+ });
257
+
258
+ test('R13 继续改/接着做仍继承同一任务', () => {
259
+ let tasks = anchor.emptyTasks();
260
+ const first = anchor.routeTask(tasks, {
261
+ sessionId: 'sess-r13',
262
+ batchKey: 'rb_a',
263
+ messageIds: ['m1'],
264
+ query: '帮我把这期口播视频的开头改一下',
265
+ selection: { seriesId: '口播' },
266
+ now: 1000,
267
+ });
268
+ tasks = first.tasks;
269
+ const next = anchor.routeTask(tasks, {
270
+ sessionId: 'sess-r13',
271
+ batchKey: 'rb_b',
272
+ messageIds: ['m2'],
273
+ query: '继续改,节奏再快一点',
274
+ now: 2000,
275
+ });
276
+ assert.equal(next.anchor.status, 'continuable');
277
+ assert.equal(next.decision.kind, 'continuation');
278
+ });
279
+
280
+ test('R14 无候选栏目时返回 none,不再要求选栏目', () => {
281
+ const read = anchor.decideRead({ enabled: true, identityConfirmed: true, anchor: null, ranges: [] });
282
+ assert.equal(read.kind, 'none');
283
+ assert.equal(read.reason, 'no_range');
284
+ });
285
+
286
+ /** 已绑定的口播任务:用于验证换任务时旧范围确实被撤销。 */
287
+ function activeVideoSession() {
288
+ const first = anchor.routeTask(anchor.emptyTasks(), {
289
+ sessionId: 'sess-switch',
290
+ batchKey: 'rb_video',
291
+ messageIds: ['m1'],
292
+ query: '帮我把这期口播视频的开头改一下',
293
+ selection: { seriesId: '口播', taskKind: '口播视频' },
294
+ now: 1000,
295
+ });
296
+ const anchorState = first.tasks.sessions['sess-switch'];
297
+ anchorState.status = 'active';
298
+ return first.tasks;
299
+ }
300
+
301
+ test('R20 带「继续」但出现新对象时,先撤销旧范围', () => {
302
+ const tasks = activeVideoSession();
303
+ const next = anchor.routeTask(tasks, {
304
+ sessionId: 'sess-switch',
305
+ batchKey: 'rb_pg_continue',
306
+ messageIds: ['m2'],
307
+ query: '接下来继续排查 PostgreSQL 连接超时',
308
+ registeredSeries: ['口播'],
309
+ now: 2000,
310
+ });
311
+ assert.equal(next.decision.kind, 'unbound');
312
+ assert.equal(next.decision.reason, 'new_task');
313
+ assert.equal(next.anchor.seriesId, undefined);
314
+ assert.equal(next.episode.previousEpisodeId, tasks.sessions['sess-switch'].episodeId);
315
+ });
316
+
317
+ test('R21 带「这个」但出现新对象时,不沿用旧范围', () => {
318
+ const tasks = activeVideoSession();
319
+ const next = anchor.routeTask(tasks, {
320
+ sessionId: 'sess-switch',
321
+ batchKey: 'rb_pg_this',
322
+ messageIds: ['m2'],
323
+ query: '现在帮我排查这个 PostgreSQL 连接超时',
324
+ registeredSeries: ['口播'],
325
+ now: 2000,
326
+ });
327
+ assert.equal(next.decision.kind, 'unbound');
328
+ assert.equal(next.decision.reason, 'new_task');
329
+ assert.equal(next.anchor.seriesId, undefined);
330
+ });
331
+
332
+ test('R22 真正的续改保留原任务范围', () => {
333
+ const tasks = activeVideoSession();
334
+ const next = anchor.routeTask(tasks, {
335
+ sessionId: 'sess-switch',
336
+ batchKey: 'rb_video_continue',
337
+ messageIds: ['m2'],
338
+ query: '继续改刚才那版脚本,开头再直接一点',
339
+ registeredSeries: ['口播'],
340
+ now: 2000,
341
+ });
342
+ assert.equal(next.decision.kind, 'continuation');
343
+ assert.equal(next.anchor.seriesId, '口播');
344
+ assert.equal(next.episode.previousEpisodeId, tasks.sessions['sess-switch'].episodeId);
345
+ });
346
+
347
+ test('R23 只指代旧稿的局部修改不被当成换任务', () => {
348
+ const tasks = activeVideoSession();
349
+ const next = anchor.routeTask(tasks, {
350
+ sessionId: 'sess-switch',
351
+ batchKey: 'rb_video_tweak',
352
+ messageIds: ['m2'],
353
+ query: '现在把开头改短一点',
354
+ registeredSeries: ['口播'],
355
+ now: 2000,
356
+ });
357
+ assert.equal(next.decision.kind, 'focus');
358
+ assert.equal(next.anchor.seriesId, '口播');
359
+ });
360
+
361
+ test('R24 继续做新领域的中文任务也算换任务', () => {
362
+ const tasks = activeVideoSession();
363
+ const next = anchor.routeTask(tasks, {
364
+ sessionId: 'sess-switch',
365
+ batchKey: 'rb_research',
366
+ messageIds: ['m2'],
367
+ query: '继续看看竞品定价',
368
+ registeredSeries: ['口播'],
369
+ now: 2000,
370
+ });
371
+ assert.equal(next.decision.kind, 'unbound');
372
+ assert.equal(next.decision.reason, 'new_task');
373
+ });
374
+
375
+ test('R25 单独说「继续」不沿用旧任务范围', () => {
376
+ const tasks = activeVideoSession();
377
+ const previous = tasks.sessions['sess-switch'];
378
+ const next = anchor.routeTask(tasks, {
379
+ sessionId: 'sess-switch',
380
+ batchKey: 'rb_weak_continue',
381
+ messageIds: ['m2'],
382
+ query: '继续',
383
+ registeredSeries: ['口播'],
384
+ now: 2000,
385
+ });
386
+ assert.equal(next.decision.kind, 'unbound');
387
+ assert.equal(next.decision.reason, 'new_task');
388
+ assert.equal(next.anchor.seriesId, undefined);
389
+ assert.equal(next.episode.previousEpisodeId, previous.episodeId);
390
+ });
391
+
392
+ test('R26 单独说「这个」不沿用旧任务范围', () => {
393
+ const tasks = activeVideoSession();
394
+ const next = anchor.routeTask(tasks, {
395
+ sessionId: 'sess-switch',
396
+ batchKey: 'rb_weak_this',
397
+ messageIds: ['m2'],
398
+ query: '这个',
399
+ registeredSeries: ['口播'],
400
+ now: 2000,
401
+ });
402
+ assert.equal(next.decision.kind, 'unbound');
403
+ assert.equal(next.decision.reason, 'new_task');
404
+ assert.equal(next.anchor.seriesId, undefined);
405
+ });
406
+
407
+ test('R27 单独说「之前」不沿用旧任务范围', () => {
408
+ const tasks = activeVideoSession();
409
+ const next = anchor.routeTask(tasks, {
410
+ sessionId: 'sess-switch',
411
+ batchKey: 'rb_weak_before',
412
+ messageIds: ['m2'],
413
+ query: '之前',
414
+ registeredSeries: ['口播'],
415
+ now: 2000,
416
+ });
417
+ assert.equal(next.decision.kind, 'unbound');
418
+ assert.equal(next.decision.reason, 'new_task');
419
+ assert.equal(next.anchor.seriesId, undefined);
420
+ });
421
+
422
+ test('R28 指代加动作的续接仍保留旧任务范围', () => {
423
+ const tasks = activeVideoSession();
424
+ const previous = tasks.sessions['sess-switch'];
425
+ const next = anchor.routeTask(tasks, {
426
+ sessionId: 'sess-switch',
427
+ batchKey: 'rb_strong_continue',
428
+ messageIds: ['m2'],
429
+ query: '把刚才那个再调一下',
430
+ registeredSeries: ['口播'],
431
+ now: 2000,
432
+ });
433
+ assert.notEqual(next.decision.kind, 'unbound');
434
+ assert.equal(next.anchor.episodeId, previous.episodeId);
435
+ assert.equal(next.anchor.seriesId, '口播');
436
+ });
437
+
438
+ test('R29 带「继续改」但对象换成新的英文主题时撤销旧范围', () => {
439
+ const tasks = activeVideoSession();
440
+ const next = anchor.routeTask(tasks, {
441
+ sessionId: 'sess-switch',
442
+ batchKey: 'rb_switch_pg_continue',
443
+ messageIds: ['m2'],
444
+ query: '继续改 PostgreSQL 连接池配置',
445
+ registeredSeries: ['口播'],
446
+ now: 2000,
447
+ });
448
+ assert.equal(next.decision.kind, 'unbound');
449
+ assert.equal(next.decision.reason, 'new_task');
450
+ assert.equal(next.anchor.seriesId, undefined);
451
+ });
452
+
453
+ test('R30 带「调整一下」但对象换了时撤销旧范围', () => {
454
+ const tasks = activeVideoSession();
455
+ const next = anchor.routeTask(tasks, {
456
+ sessionId: 'sess-switch',
457
+ batchKey: 'rb_switch_pg_adjust',
458
+ messageIds: ['m2'],
459
+ query: '调整一下 PostgreSQL 连接池配置',
460
+ registeredSeries: ['口播'],
461
+ now: 2000,
462
+ });
463
+ assert.equal(next.decision.kind, 'unbound');
464
+ assert.equal(next.decision.reason, 'new_task');
465
+ });
466
+
467
+ test('R31 带「继续改」但明确指向新对象动作时撤销旧范围', () => {
468
+ const tasks = activeVideoSession();
469
+ const next = anchor.routeTask(tasks, {
470
+ sessionId: 'sess-switch',
471
+ batchKey: 'rb_switch_db_code',
472
+ messageIds: ['m2'],
473
+ query: '继续改数据库连接超时的处理代码',
474
+ registeredSeries: ['口播'],
475
+ now: 2000,
476
+ });
477
+ assert.equal(next.decision.kind, 'unbound');
478
+ assert.equal(next.decision.reason, 'new_task');
479
+ });
480
+
481
+ /** 复核复现:句首带「继续」不能成为沿用旧正文的通行证。 */
482
+ for (const [label, query] of [
483
+ ['R32 继续改数据库连接池配置', '继续改数据库连接池配置'],
484
+ ['R33 继续优化数据库连接池', '继续优化数据库连接池'],
485
+ ['R34 继续写数据库故障报告', '继续写数据库故障报告'],
486
+ ['R35 这个数据库连接池配置怎么改', '这个数据库连接池配置怎么改'],
487
+ ]) {
488
+ test(`${label}:与旧任务没有对象交集时不继承旧范围`, () => {
489
+ const tasks = activeVideoSession();
490
+ const next = anchor.routeTask(tasks, {
491
+ sessionId: 'sess-switch',
492
+ batchKey: `rb_${label.slice(0, 3)}_db`,
493
+ messageIds: ['m2'],
494
+ query,
495
+ registeredSeries: ['口播'],
496
+ now: 2000,
497
+ });
498
+ assert.equal(next.decision.kind, 'unbound');
499
+ assert.equal(next.decision.reason, 'new_task');
500
+ assert.equal(next.anchor.seriesId, undefined);
501
+ });
502
+ }
503
+
504
+ test('R41 旧稿提到但明确换到排查任务时不再绑定旧栏目', () => {
505
+ const tasks = activeVideoSession();
506
+ for (const query of ['排查 PostgreSQL 连接池故障,旧稿先放着', '这版数据库连接池配置怎么改']) {
507
+ const next = anchor.routeTask(tasks, {
508
+ sessionId: 'sess-switch',
509
+ batchKey: `rb_${query.length}`,
510
+ messageIds: ['m2'],
511
+ query,
512
+ registeredSeries: ['口播'],
513
+ now: 2000,
514
+ });
515
+ assert.equal(next.decision.kind, 'unbound', query);
516
+ assert.equal(next.anchor.seriesId, undefined, query);
517
+ }
518
+ });
519
+
520
+ test('R42 栏目只被提到、否定或搁置时都不算用户选中', () => {
521
+ assert.equal(anchor.seriesChosenBy('把口播开头改一下', '口播'), true);
522
+ assert.equal(anchor.seriesChosenBy('先不做口播了', '口播'), false);
523
+ assert.equal(anchor.seriesChosenBy('不要按口播的规矩来', '口播'), false);
524
+ assert.equal(anchor.seriesChosenBy('日志里提到口播吗', '口播'), false);
525
+ assert.equal(anchor.seriesChosenBy('先放着口播,改数据库', '口播'), false);
526
+ });
@@ -20,7 +20,47 @@ assert.ok(evolution, '项目开关没有注册');
20
20
  assert.equal(evolution.name, 'conversation.session.header.utilities');
21
21
  assert.equal(evolution.order, 30);
22
22
 
23
- const { metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf } = seen;
23
+ // 记忆面板占整框浮层,不挤在会话列里。
24
+ const panel = registrations.find((item) => item.id === 'lite-memory:panel');
25
+ assert.ok(panel, '记忆面板没有注册');
26
+ assert.equal(panel.name, 'shell.overlay');
27
+ assert.equal(panel.order, 30);
28
+
29
+ const { metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf } = seen;
30
+
31
+ // 图谱只按真实数据画:共享标签成枢纽,显式关联成边,位置由 id 决定。
32
+ const graph = graphOf([
33
+ { id: 'mem_aaaaaaaa', title: 'A', tags: ['发布'], topicKey: '上线', experimental: false, links: [{ id: 'mem_bbbbbbbb' }] },
34
+ { id: 'mem_bbbbbbbb', title: 'B', tags: ['发布'], topicKey: '上线', experimental: true, links: [] },
35
+ { id: 'mem_cccccccc', title: 'C', tags: ['环境'], topicKey: '', experimental: false, links: [] },
36
+ ]);
37
+ assert.equal(graph.nodes.filter((node) => node.kind === 'memory').length, 3);
38
+ assert.equal(graph.nodes.some((node) => node.id === 'tag:发布'), true);
39
+ assert.equal(graph.nodes.some((node) => node.id === 'tag:环境'), true);
40
+ assert.equal(graph.nodes.some((node) => node.id === 'topic:上线'), true);
41
+ assert.equal(graph.edges.filter((edge) => edge.type === 'tag').length, 3);
42
+ assert.equal(graph.edges.filter((edge) => edge.type === 'link').length, 1);
43
+ assert.deepEqual(graphOf([{ id: 'mem_aaaaaaaa', title: 'A' }]).nodes[0].x, graphOf([{ id: 'mem_aaaaaaaa', title: 'A' }]).nodes[0].x);
44
+ const pair = graphOf([{ id: 'mem_aaaaaaaa', title: 'A' }, { id: 'mem_bbbbbbbb', title: 'B' }]);
45
+ assert.notEqual(pair.nodes[0].x, pair.nodes[1].x);
46
+ assert.notEqual(pair.nodes[0].y, pair.nodes[1].y);
47
+
48
+ // 经验图谱只按共享的适用条件相连,不画没有依据的关系。
49
+ const rules = [
50
+ { id: 'ev_aaaaaaaa', title: 'A', state: 'active', conditions: [{ key: 'seriesId', value: '口播' }] },
51
+ { id: 'ev_bbbbbbbb', title: 'B', state: 'trial', conditions: [{ key: 'seriesId', value: '口播' }, { key: 'phase', value: '初稿' }] },
52
+ { id: 'ev_cccccccc', title: 'C', state: 'pending', conditions: [{ key: 'phase', value: '初稿' }] },
53
+ ];
54
+ const ruleGraph = ruleGraphOf(rules);
55
+ assert.equal(ruleGraph.nodes.filter((node) => node.kind === 'rule').length, 3);
56
+ assert.equal(ruleGraph.nodes.some((node) => node.id === 'condition:seriesId:口播'), true);
57
+ assert.equal(ruleGraph.nodes.some((node) => node.id === 'condition:phase:初稿'), true);
58
+ assert.equal(ruleGraph.edges.length, 4);
59
+ assert.equal(ruleGraph.edges.every((edge) => edge.type === 'condition'), true);
60
+ assert.equal(ruleGraph.nodes.some((node) => node.kind === 'tag' || node.kind === 'topic'), false);
61
+ assert.deepEqual(ruleGraphOf(rules).nodes.map((node) => [node.id, node.x, node.y]), ruleGraph.nodes.map((node) => [node.id, node.x, node.y]));
62
+ assert.equal(ruleGraphOf([{ id: 'ev_aaaaaaaa', title: 'A', state: 'active', conditions: [] }]).edges.length, 0);
63
+
24
64
  const settled = (meta, extra = {}) => ({ kind: 'tool-result', meta, isError: false, content: [], ...extra });
25
65
 
26
66
  // 元数据只从落定结果里取,错误块和旧块一律不认。
@@ -53,20 +93,22 @@ assert.equal(refs.length, 2);
53
93
  assert.equal(refs[1].status, 'archived');
54
94
  assert.equal(refs[0].exposure, 'reference');
55
95
 
56
- // 一轮里 PTC 子结果也算;同 id 同版同用途去重。
96
+ // 一轮里 PTC 子结果也算;同 id 同用途只留一条,位置不变、内容取最新。
57
97
  const nodes = [
58
98
  { root: settled({ kind: 'memory-search', refs: [
59
99
  { id: 'mem_aaaaaaaa', title: 'A', scope: 'project', revision: 1, status: 'ok' },
60
100
  ] }) },
61
101
  { root: settled({ kind: 'memory-read', id: 'mem_aaaaaaaa', title: 'A2', scope: 'project', revision: 2, status: 'ok' }, { subCalls: [settled({ kind: 'memory-read', id: 'mem_aaaaaaaa', title: 'A3', scope: 'project', revision: 2, status: 'ok' })] }) },
102
+ { root: settled({ kind: 'memory-read', id: 'mem_bbbbbbbb', title: 'B', scope: 'global', revision: 1, status: 'ok' }) },
62
103
  ];
63
104
  const turn = dedupe(citationsOfTurn(nodes));
64
105
  assert.equal(turn.length, 2);
65
- assert.equal(turn[0].revision, 1);
66
- assert.equal(turn[1].revision, 2);
67
- assert.equal(turn[1].title, 'A2');
106
+ assert.equal(turn[0].id, 'mem_aaaaaaaa');
107
+ assert.equal(turn[0].revision, 2);
108
+ assert.equal(turn[0].title, 'A3');
109
+ assert.equal(turn[1].id, 'mem_bbbbbbbb');
68
110
 
69
111
  // 旧行没有元数据:什么也不产出,绝不从文案猜 id。
70
112
  assert.deepEqual(citationsOfTurn([{ root: null }, { root: { kind: 'tool-result', content: [{ type: 'text', text: 'memory · saved "A" mem_aaaaaaaa' }] } }]), []);
71
113
 
72
- console.log('client.test.mjs: 10 passed');
114
+ console.log('client.test.mjs: 11 passed');