dsh-lite-memory 0.3.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -17
- package/TRIGGERS.md +18 -14
- package/anchor.js +518 -0
- package/bench/bench.mjs +145 -0
- package/bench/evolution.mjs +123 -0
- package/client.js +1519 -33
- package/docs/BENCH.md +74 -0
- package/docs/RELEASE-0.11.0.md +27 -0
- package/docs/RELEASE-0.11.1.md +11 -0
- package/docs/RELEASE-0.12.0.md +15 -0
- package/docs/RELEASE-0.12.1.md +17 -0
- package/docs/RELEASE-0.12.2.md +17 -0
- package/docs/RELEASE-0.13.0.md +7 -0
- package/docs/RELEASE-0.14.0.md +7 -0
- package/docs/RELEASE-0.15.0.md +8 -0
- package/index.js +2080 -76
- package/learning.js +585 -0
- package/package.json +10 -2
- package/state.js +1476 -0
- package/store.js +2967 -244
- package/test/anchor.test.mjs +436 -0
- package/test/client.test.mjs +114 -0
- package/test/contract.test.mjs +880 -0
- package/test/control.test.mjs +278 -0
- package/test/crossing.test.mjs +372 -0
- package/test/evolution-read.test.mjs +448 -0
- package/test/fixtures/legacy-v1/archive/mem_444444444444.json +18 -0
- package/test/fixtures/legacy-v1/entries/mem_111111111111.md +2 -0
- package/test/fixtures/legacy-v1/entries/mem_222222222222.md +1 -0
- package/test/fixtures/legacy-v1/entries/mem_333333333333.md +1 -0
- package/test/fixtures/legacy-v1/index.json +63 -0
- package/test/learning.test.mjs +772 -0
- package/test/lexical.test.mjs +33 -0
- package/test/recall.test.mjs +449 -0
- package/test/scenario.test.mjs +184 -0
- package/test/state.test.mjs +485 -0
- package/test/store.test.mjs +816 -115
- package/test/view.test.mjs +257 -0
- package/view.js +616 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-lexical-'));
|
|
7
|
+
const store = await import('../store.js');
|
|
8
|
+
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-lexical-scope-'));
|
|
10
|
+
const scope = store.scopeOf(dir);
|
|
11
|
+
const add = (title, extra = {}) => store.upsert(scope, {
|
|
12
|
+
applicability: 'all_in_scope',
|
|
13
|
+
body: `正文:${title}`,
|
|
14
|
+
summary: extra.summary || `${title}的摘要`,
|
|
15
|
+
tags: extra.tags || [],
|
|
16
|
+
resolution: 'distinct',
|
|
17
|
+
title,
|
|
18
|
+
});
|
|
19
|
+
add('digital signal 调试', { tags: ['音频'] });
|
|
20
|
+
add('怎么做发布检查', { tags: ['发布'] });
|
|
21
|
+
add('git 提交前跑检查', { tags: ['git'] });
|
|
22
|
+
|
|
23
|
+
/* 英文按整词:git 不能因为共享 gi/it 命中 digital。 */
|
|
24
|
+
assert.equal(store.search(scope, 'git', 5).some((row) => row.title.includes('digital')), false);
|
|
25
|
+
assert.equal(store.search(scope, 'digital', 5).some((row) => row.title.includes('digital')), true);
|
|
26
|
+
|
|
27
|
+
/* 只重合「怎么」这类高频词不算命中。 */
|
|
28
|
+
assert.equal(store.search(scope, '这个怎么弄', 5).length, 0);
|
|
29
|
+
|
|
30
|
+
/* 实词仍然命中。 */
|
|
31
|
+
assert.equal(store.search(scope, '发布检查', 5).some((row) => row.title.includes('怎么做发布检查')), true);
|
|
32
|
+
|
|
33
|
+
console.log('lexical.test.mjs: all passed');
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-recall-'));
|
|
7
|
+
const { apply } = await import('../index.js');
|
|
8
|
+
const store = await import('../store.js');
|
|
9
|
+
|
|
10
|
+
const HEADER = '已有记忆可能相关';
|
|
11
|
+
const CARD_LINE = /^- \[[^\]]+\] 「.+?」.* · mem_[a-z0-9]+(?:|.*)?$/;
|
|
12
|
+
const CARD_LINE_M = new RegExp(CARD_LINE.source, 'm');
|
|
13
|
+
|
|
14
|
+
const effect = (fn) => {
|
|
15
|
+
const dispose = fn();
|
|
16
|
+
return typeof dispose === 'function' ? dispose : () => {};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function boot(mode = 'adaptive') {
|
|
20
|
+
const defs = new Map();
|
|
21
|
+
const handlers = new Map();
|
|
22
|
+
const promptContexts = [];
|
|
23
|
+
const ctx = {
|
|
24
|
+
tools: { register: (def) => { defs.set(def.name, def); return () => {}; } },
|
|
25
|
+
effect,
|
|
26
|
+
inject: () => () => {},
|
|
27
|
+
get: (name) => (name === 'systemPrompt'
|
|
28
|
+
? { context: (entry) => { promptContexts.push(entry); return () => {}; } }
|
|
29
|
+
: name === 'agents' ? { currentInitiator: () => null } : undefined),
|
|
30
|
+
on: (event, handler) => {
|
|
31
|
+
const list = handlers.get(event) || [];
|
|
32
|
+
list.push(handler);
|
|
33
|
+
handlers.set(event, list);
|
|
34
|
+
return () => {};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
apply(ctx, { mode });
|
|
38
|
+
const debug = globalThis.__liteMemoryRecall;
|
|
39
|
+
const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
|
|
40
|
+
assert.ok(provider, 'recall provider registered');
|
|
41
|
+
assert.equal(provider.order, 60);
|
|
42
|
+
assert.equal(provider.interpolate, false);
|
|
43
|
+
assert.equal(provider.required, false);
|
|
44
|
+
const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
|
|
45
|
+
const render = (name, args, value) => defs.get(name).output.render(args, value)[0].text;
|
|
46
|
+
const preStep = (payload, decision) => {
|
|
47
|
+
const handler = handlers.get('agent/pre-step').at(-1);
|
|
48
|
+
return handler(payload, async () => decision);
|
|
49
|
+
};
|
|
50
|
+
return { defs, handlers, provider, exec, render, preStep, debug };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const signal = () => new AbortController().signal;
|
|
54
|
+
|
|
55
|
+
let messageSeq = 0;
|
|
56
|
+
function human(body, options = {}) {
|
|
57
|
+
messageSeq += 1;
|
|
58
|
+
return {
|
|
59
|
+
role: 'user',
|
|
60
|
+
id: options.id ?? `msg-${messageSeq}`,
|
|
61
|
+
source: { kind: 'user' },
|
|
62
|
+
content: [{ type: 'text', text: body }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const synthetic = (kind, body) => ({
|
|
66
|
+
role: 'user', id: `synth-${(messageSeq += 1)}`, source: { kind }, content: [{ type: 'text', text: body }],
|
|
67
|
+
});
|
|
68
|
+
const enter = (...messages) => ({ kind: 'enter', messages });
|
|
69
|
+
const prepare = async (runtime, agent, query, turn = 't1') => {
|
|
70
|
+
const sig = signal();
|
|
71
|
+
await runtime.preStep({ agent, signal: sig, turn }, enter(human(query)));
|
|
72
|
+
return sig;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const mkdir = (label) => fs.mkdtempSync(path.join(os.tmpdir(), `lite-memory-recall-${label}-`));
|
|
76
|
+
const makeAgent = (id, dir) => ({ id, cwd: dir, session: { id: `session-${id}`, header: { cwd: dir } } });
|
|
77
|
+
const save = (scope, fields) => {
|
|
78
|
+
const result = store.upsert(scope, {
|
|
79
|
+
applicability: 'all_in_scope',
|
|
80
|
+
body: `正文:${fields.title}`,
|
|
81
|
+
summary: fields.summary ?? `${fields.title}的摘要`,
|
|
82
|
+
...fields,
|
|
83
|
+
});
|
|
84
|
+
return { ...result, id: result.record?.id ?? result.id };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const cwdA = mkdir('a');
|
|
88
|
+
const scopeA = store.scopeOf(cwdA);
|
|
89
|
+
const global = store.globalScope();
|
|
90
|
+
|
|
91
|
+
const hotReload = save(scopeA, { title: '插件改完先试热刷新', tags: ['插件', '热刷新'], summary: '改完先热刷新,覆盖不到再选生效方式' });
|
|
92
|
+
const installPath = save(scopeA, { title: '插件安装位置', tags: ['插件', '安装'], summary: '装上后文件落在用户目录的插件包里' });
|
|
93
|
+
const crash = save(scopeA, { title: '插件刷新崩溃排查', tags: ['插件', '崩溃'], summary: '先看日志末尾有没有 reload 行' });
|
|
94
|
+
const publish = save(scopeA, { title: '发布前跑文案检查', tags: ['发布'], summary: '交付前把界面文案过一遍 lint' });
|
|
95
|
+
const unrelated = save(scopeA, { title: '本地视频侧边栏播放', tags: ['视频'], summary: '本地文件也要能在侧边栏播' });
|
|
96
|
+
const linuxOnly = save(scopeA, {
|
|
97
|
+
title: '插件在 Linux CI 里构建',
|
|
98
|
+
tags: ['插件', '构建'],
|
|
99
|
+
when: '',
|
|
100
|
+
applicability: 'conditional',
|
|
101
|
+
conditions: [{ key: 'targetOs', value: 'linux' }],
|
|
102
|
+
summary: '只在 Linux CI 上跑构建检查',
|
|
103
|
+
});
|
|
104
|
+
const unresolved = save(scopeA, { title: '发布前要检查什么', tags: ['发布', '检查'], when: '发布前', applicability: 'unresolved' });
|
|
105
|
+
const globalHot = save(global, { title: '插件改动后先热刷新', tags: ['插件', '热刷新'], summary: '全局规矩:先热刷新再谈重启' });
|
|
106
|
+
|
|
107
|
+
/** 预算用:6 条都命中「发布」,单卡够大,只能装下少数几张。 */
|
|
108
|
+
const longEntries = [];
|
|
109
|
+
const longParts = ['文案', '图标', '包体', '权限', '链接', '版本'];
|
|
110
|
+
for (let i = 0; i < 6; i += 1) {
|
|
111
|
+
longEntries.push(save(scopeA, {
|
|
112
|
+
title: `发布流程${longParts[i]}检查${'长'.repeat(35)}`,
|
|
113
|
+
summary: `这一步的说明比较长,${'细'.repeat(50)}`,
|
|
114
|
+
tags: ['发布', longParts[i]],
|
|
115
|
+
resolution: 'distinct',
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
const bigBodies = [];
|
|
119
|
+
for (let i = 0; i < 9; i += 1) {
|
|
120
|
+
bigBodies.push(save(scopeA, {
|
|
121
|
+
title: `大正文样本${i + 1}`,
|
|
122
|
+
summary: '读取预算测试',
|
|
123
|
+
tags: ['大正文'],
|
|
124
|
+
resolution: 'distinct',
|
|
125
|
+
body: `样本${i + 1}:${'正文内容'.repeat(80)}`,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const bootA = boot('adaptive');
|
|
130
|
+
|
|
131
|
+
// A01/A02:真人的第一句请求先准备,准备好之前 provider 是空的。
|
|
132
|
+
const agentA = makeAgent('agent-a', cwdA);
|
|
133
|
+
const signalA = signal();
|
|
134
|
+
assert.equal(bootA.provider.text({ agent: agentA, signal: signalA }), '');
|
|
135
|
+
const decisionA = enter(human('插件改完怎么让它生效?'));
|
|
136
|
+
await bootA.preStep({ agent: agentA, signal: signalA, turn: 't1' }, decisionA);
|
|
137
|
+
const preparedText = bootA.provider.text({ agent: agentA, signal: signalA });
|
|
138
|
+
assert.ok(preparedText.startsWith(HEADER), 'recall text starts with header');
|
|
139
|
+
assert.match(preparedText, /插件改完先试热刷新/);
|
|
140
|
+
assert.match(preparedText, CARD_LINE_M);
|
|
141
|
+
assert.ok(Buffer.byteLength(preparedText) <= 1200, 'snapshot stays inside card budget');
|
|
142
|
+
const cards = bootA.provider.text({ agent: agentA, signal: signalA }).split('\n').slice(1);
|
|
143
|
+
assert.ok(cards.length <= 3);
|
|
144
|
+
for (const line of cards) assert.match(line, CARD_LINE);
|
|
145
|
+
assert.doesNotMatch(preparedText, /第 .* 版/);
|
|
146
|
+
assert.equal(bootA.provider.text({ agent: agentA, signal: signalA }), preparedText, 'frozen between calls');
|
|
147
|
+
|
|
148
|
+
// A03:provider 纯内存,不写盘、不动 hits。
|
|
149
|
+
const statePath = path.join(scopeA.dir, 'state.json');
|
|
150
|
+
const before = fs.readFileSync(statePath);
|
|
151
|
+
for (let i = 0; i < 20; i += 1) bootA.provider.text({ agent: agentA, signal: signalA });
|
|
152
|
+
assert.deepEqual(fs.readFileSync(statePath), before);
|
|
153
|
+
const storedHits = store.loadIndex(scopeA).entries.reduce((sum, item) => sum + (item.hits || 0), 0);
|
|
154
|
+
assert.equal(storedHits, 0);
|
|
155
|
+
|
|
156
|
+
// A04:不同 Agent/会话各自准备,不串卡片。
|
|
157
|
+
const cwdB = mkdir('b');
|
|
158
|
+
const scopeB = store.scopeOf(cwdB);
|
|
159
|
+
const video = save(scopeB, { title: '本地视频在侧边栏播放', tags: ['视频', '播放'], summary: '不受会话目录限制' });
|
|
160
|
+
const agentB = makeAgent('agent-b', cwdB);
|
|
161
|
+
const signalB = signal();
|
|
162
|
+
await bootA.preStep({ agent: agentB, signal: signalB, turn: 't1' }, enter(human('本地视频在侧边栏怎么播放?')));
|
|
163
|
+
const textB = bootA.provider.text({ agent: agentB, signal: signalB });
|
|
164
|
+
assert.match(textB, new RegExp(video.title));
|
|
165
|
+
assert.doesNotMatch(textB, /插件改完先试热刷新/);
|
|
166
|
+
assert.equal(bootA.provider.text({ agent: agentA, signal: signalA }), preparedText, 'agent A untouched');
|
|
167
|
+
assert.notEqual(bootA.provider.text({ agent: agentB, signal: signalB }), preparedText);
|
|
168
|
+
|
|
169
|
+
// A05:取消/超时/迟到结果都不能覆盖新请求。
|
|
170
|
+
{
|
|
171
|
+
const agent = makeAgent('agent-abort', cwdA);
|
|
172
|
+
const ctrl = new AbortController();
|
|
173
|
+
await bootA.preStep({ agent, signal: ctrl.signal, turn: 't1' }, enter(human('插件改完怎么让它生效?')));
|
|
174
|
+
assert.ok(bootA.provider.text({ agent, signal: ctrl.signal }));
|
|
175
|
+
ctrl.abort();
|
|
176
|
+
assert.equal(bootA.provider.text({ agent, signal: ctrl.signal }), '');
|
|
177
|
+
|
|
178
|
+
const staleAgent = makeAgent('agent-stale', cwdA);
|
|
179
|
+
const slower = new Promise((resolve) => setTimeout(() => resolve(enter(human('插件改完怎么让它生效?'))), 30));
|
|
180
|
+
const stale = bootA.preStep({ agent: staleAgent, signal: signal(), turn: 't1' }, await slower);
|
|
181
|
+
const newerSignal = signal();
|
|
182
|
+
await bootA.preStep({ agent: staleAgent, signal: newerSignal, turn: 't2' }, enter(human('本地视频侧边栏播放')));
|
|
183
|
+
await stale;
|
|
184
|
+
const state = bootA.debug.preparedOf(staleAgent);
|
|
185
|
+
assert.ok(state, 'newer request stays prepared');
|
|
186
|
+
assert.match(state.query, /本地视频/);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// A06:模式策略;关掉时不准备、也不返回内容。
|
|
190
|
+
{
|
|
191
|
+
const toolOnly = boot('tool_only');
|
|
192
|
+
const agent = makeAgent('agent-tool', cwdA);
|
|
193
|
+
const toolSig = await prepare(toolOnly, agent, '插件改完怎么让它生效?');
|
|
194
|
+
assert.equal(toolOnly.provider.text({ agent, signal: toolSig }), '');
|
|
195
|
+
const off = boot('off');
|
|
196
|
+
const agentOff = makeAgent('agent-off', cwdA);
|
|
197
|
+
const offSig = await prepare(off, agentOff, '插件改完怎么让它生效?');
|
|
198
|
+
assert.equal(off.provider.text({ agent: agentOff, signal: offSig }), '');
|
|
199
|
+
const disabled = await off.exec('memory_search', { query: '插件' }, agentOff);
|
|
200
|
+
assert.equal(disabled.status, 'memory_disabled');
|
|
201
|
+
assert.equal(off.render('memory_search', {}, disabled), 'memory · 记忆检索已关闭');
|
|
202
|
+
const readDisabled = await off.exec('memory_read', { id: hotReload.id }, agentOff);
|
|
203
|
+
assert.equal(readDisabled.status, 'memory_disabled');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// A07:合成消息不算真人请求,不准备也不开窗。
|
|
207
|
+
{
|
|
208
|
+
const agent = makeAgent('agent-synth', cwdA);
|
|
209
|
+
const webSig = signal();
|
|
210
|
+
await bootA.preStep({ agent, signal: webSig, turn: 't1' }, enter(synthetic('web', '插件改完怎么让它生效?做一个网页搜索')));
|
|
211
|
+
assert.equal(bootA.provider.text({ agent, signal: webSig }), '');
|
|
212
|
+
const goalSig = signal();
|
|
213
|
+
await bootA.preStep({ agent, signal: goalSig, turn: 't2' }, enter(synthetic('goal', '插件改完怎么让它生效')));
|
|
214
|
+
assert.equal(bootA.provider.text({ agent, signal: goalSig }), '');
|
|
215
|
+
assert.equal(bootA.debug.windowOf(agent), null);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A08:真的无关时一个字都不注入。
|
|
219
|
+
{
|
|
220
|
+
const agent = makeAgent('agent-unrelated', cwdA);
|
|
221
|
+
const sig = await prepare(bootA, agent, '周末去山里走走');
|
|
222
|
+
assert.equal(bootA.provider.text({ agent, signal: sig }), '');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A09:条件不匹配、适用范围没解决的条目绝不自动召回。
|
|
226
|
+
{
|
|
227
|
+
const cwdC = mkdir('c');
|
|
228
|
+
const scopeC = store.scopeOf(cwdC);
|
|
229
|
+
save(scopeC, {
|
|
230
|
+
title: '只在 Linux 上跑构建检查',
|
|
231
|
+
tags: ['构建'],
|
|
232
|
+
applicability: 'conditional',
|
|
233
|
+
conditions: [{ key: 'targetOs', value: 'linux' }],
|
|
234
|
+
});
|
|
235
|
+
save(scopeC, { title: '发布前检查清单', tags: ['发布'], when: '发布前', applicability: 'unresolved' });
|
|
236
|
+
const agent = makeAgent('agent-cond', cwdC);
|
|
237
|
+
const sig = await prepare(bootA, agent, '发布前检查清单和 Linux 构建怎么做');
|
|
238
|
+
assert.equal(bootA.provider.text({ agent, signal: sig }), '');
|
|
239
|
+
assert.notEqual(linuxOnly.id, unresolved.id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A10:整卡装预算;装不下就整卡省略,不截断半个标题。
|
|
243
|
+
{
|
|
244
|
+
const agent = makeAgent('agent-budget', cwdA);
|
|
245
|
+
const sig = await prepare(bootA, agent, '发布流程每一步要检查什么?');
|
|
246
|
+
const snapshot = bootA.provider.text({ agent, signal: sig });
|
|
247
|
+
assert.ok(snapshot.length > 0);
|
|
248
|
+
assert.ok(Buffer.byteLength(snapshot) <= 1200);
|
|
249
|
+
const lines = snapshot.split('\n').slice(1);
|
|
250
|
+
assert.ok(lines.length >= 1 && lines.length <= 3);
|
|
251
|
+
for (const line of lines) {
|
|
252
|
+
assert.match(line, CARD_LINE);
|
|
253
|
+
assert.ok(line.trimEnd().endsWith('版') || line.includes('|'), 'card is not cut mid-title');
|
|
254
|
+
const title = line.match(/「(.+?)」/)?.[1] ?? '';
|
|
255
|
+
assert.ok(title.length > 0);
|
|
256
|
+
if (!longEntries.some((item) => item.record?.title === title) && !/插件|发布前跑/.test(title)) {
|
|
257
|
+
throw new Error(`unexpected card: ${line}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// A11/A12/A13:读正文共用 8192 B / 3 条正文 / 8 次处理,重复读只回短句柄。
|
|
263
|
+
{
|
|
264
|
+
const agent = makeAgent('agent-ledger', cwdA);
|
|
265
|
+
await bootA.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('大正文样本怎么用')));
|
|
266
|
+
const first = await bootA.exec('memory_read', { id: bigBodies[0].id }, agent);
|
|
267
|
+
assert.equal(first.status, 'ok');
|
|
268
|
+
assert.ok(Buffer.byteLength(first.body) > 900);
|
|
269
|
+
const repeat = await bootA.exec('memory_read', { id: bigBodies[0].id }, agent);
|
|
270
|
+
assert.equal(repeat.repeat, true);
|
|
271
|
+
assert.equal(repeat.body, '');
|
|
272
|
+
assert.match(bootA.render('memory_read', {}, repeat), /本请求已读过/);
|
|
273
|
+
const second = await bootA.exec('memory_read', { id: bigBodies[1].id }, agent);
|
|
274
|
+
assert.equal(second.status, 'ok');
|
|
275
|
+
const third = await bootA.exec('memory_read', { id: bigBodies[2].id }, agent);
|
|
276
|
+
assert.equal(third.status, 'ok');
|
|
277
|
+
const fourth = await bootA.exec('memory_read', { id: bigBodies[3].id }, agent);
|
|
278
|
+
assert.equal(fourth.status, 'budget_exhausted');
|
|
279
|
+
assert.equal(fourth.body || '', '');
|
|
280
|
+
assert.equal(fourth.id, '');
|
|
281
|
+
assert.equal(bootA.render('memory_read', {}, fourth), 'memory · 本请求的记忆额度已用完');
|
|
282
|
+
const win = bootA.debug.windowOf(agent);
|
|
283
|
+
assert.ok(win.bytes <= 8192);
|
|
284
|
+
assert.equal(win.bodies, 3);
|
|
285
|
+
|
|
286
|
+
const agentReads = makeAgent('agent-reads', cwdA);
|
|
287
|
+
await bootA.preStep({ agent: agentReads, signal: signal(), turn: 't1' }, enter(human('大正文样本怎么用')));
|
|
288
|
+
const firstRead = await bootA.exec('memory_read', { id: bigBodies[3].id }, agentReads);
|
|
289
|
+
assert.equal(firstRead.status, 'ok');
|
|
290
|
+
for (let i = 0; i < 7; i += 1) {
|
|
291
|
+
const row = await bootA.exec('memory_read', { id: bigBodies[3].id }, agentReads);
|
|
292
|
+
assert.equal(row.repeat, true, `repeat ${i + 1}`);
|
|
293
|
+
}
|
|
294
|
+
const capped = await bootA.exec('memory_read', { id: bigBodies[3].id }, agentReads);
|
|
295
|
+
assert.equal(capped.status, 'budget_exhausted');
|
|
296
|
+
assert.equal(capped.title, undefined);
|
|
297
|
+
assert.equal(bootA.debug.windowOf(agentReads).reads, 8);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// A12:额度状态与冻结快照分开;清掉快照后重准备不重新分配。
|
|
301
|
+
{
|
|
302
|
+
const agent = makeAgent('agent-preserve', cwdA);
|
|
303
|
+
const sig = signal();
|
|
304
|
+
await bootA.preStep({ agent, signal: sig, turn: 't1' }, enter(human('插件改完怎么让它生效?')));
|
|
305
|
+
const win = bootA.debug.windowOf(agent);
|
|
306
|
+
const used = win.bytes;
|
|
307
|
+
assert.ok(used > 0);
|
|
308
|
+
const turnEnd = bootA.handlers.get('agent/turn-stopping').at(-1);
|
|
309
|
+
turnEnd({ turn: 't1', signal: sig });
|
|
310
|
+
assert.equal(bootA.provider.text({ agent, signal: sig }), '');
|
|
311
|
+
await bootA.preStep({ agent, signal: sig, turn: 't2' }, enter(human('插件改完怎么让它生效?再确认一下')));
|
|
312
|
+
assert.ok(bootA.provider.text({ agent, signal: sig }).startsWith(HEADER));
|
|
313
|
+
assert.equal(win.bytes, used, 'same window does not re-allocate for already provided cards');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// A14:上一轮注入过的文字不会被当成这一轮的来源;只有合成消息的新 turn 不继承。
|
|
317
|
+
{
|
|
318
|
+
const agent = makeAgent('agent-stale-section', cwdA);
|
|
319
|
+
const sig1 = signal();
|
|
320
|
+
await bootA.preStep({ agent, signal: sig1, turn: 't1' }, enter(human('插件改完怎么让它生效?')));
|
|
321
|
+
assert.ok(bootA.provider.text({ agent, signal: sig1 }));
|
|
322
|
+
const sig2 = signal();
|
|
323
|
+
await bootA.preStep({ agent, signal: sig2, turn: 't2' }, enter(synthetic('tool', '命令输出:done')));
|
|
324
|
+
assert.equal(bootA.provider.text({ agent, signal: sig2 }), '');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// A15:忘掉之后旧快照不复活;下一次请求不再带它。
|
|
328
|
+
{
|
|
329
|
+
const cwdD = mkdir('d');
|
|
330
|
+
const scopeD = store.scopeOf(cwdD);
|
|
331
|
+
const doomed = save(scopeD, { title: '插件改完要重启应用', tags: ['插件', '重启'], summary: '旧结论:必须重启' });
|
|
332
|
+
const agent = makeAgent('agent-purge', cwdD);
|
|
333
|
+
const sig = signal();
|
|
334
|
+
await bootA.preStep({ agent, signal: sig, turn: 't1' }, enter(human('插件改完怎么让它生效?')));
|
|
335
|
+
assert.match(bootA.provider.text({ agent, signal: sig }), new RegExp(doomed.id));
|
|
336
|
+
const purged = await bootA.exec('memory_forget', { id: doomed.id, mode: 'purge' }, agent);
|
|
337
|
+
assert.equal(purged.mode, 'purged');
|
|
338
|
+
assert.match(bootA.provider.text({ agent, signal: sig }), new RegExp(doomed.id), '已提交的旧快照不回头改写');
|
|
339
|
+
const sig2 = signal();
|
|
340
|
+
await bootA.preStep({ agent, signal: sig2, turn: 't2' }, enter(human('插件改完怎么让它生效?')));
|
|
341
|
+
const after = bootA.provider.text({ agent, signal: sig2 });
|
|
342
|
+
assert.doesNotMatch(after, new RegExp(doomed.id));
|
|
343
|
+
assert.doesNotMatch(after, /插件改完要重启应用/);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 退出清理:disposed 之后 provider 一律空。
|
|
347
|
+
{
|
|
348
|
+
const agent = makeAgent('agent-dispose', cwdA);
|
|
349
|
+
const sig = signal();
|
|
350
|
+
await bootA.preStep({ agent, signal: sig, turn: 't1' }, enter(human('插件改完怎么让它生效?')));
|
|
351
|
+
const disposed = bootA.handlers.get('agent/disposed').at(-1);
|
|
352
|
+
disposed({ agent });
|
|
353
|
+
assert.equal(bootA.provider.text({ agent, signal: sig }), '');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// A16:全局层被彻底删除后,工具续步立刻重投影;冻结的旧卡片不复活,新真人任务重算。
|
|
357
|
+
{
|
|
358
|
+
const globalNote = save(global, { title: '全局发布前文案核对', tags: ['发布', '核对'], summary: '全局规则:发布前先核对文案' });
|
|
359
|
+
const agent = makeAgent('agent-global-purge', cwdA);
|
|
360
|
+
const sig = signal();
|
|
361
|
+
await bootA.preStep({ agent, signal: sig, turn: 't1' }, enter(human('发布前跑文案检查')));
|
|
362
|
+
assert.match(bootA.provider.text({ agent, signal: sig }), new RegExp(globalNote.id));
|
|
363
|
+
const purged = store.forget(global, globalNote.id, { mode: 'purge' });
|
|
364
|
+
assert.equal(purged.mode, 'purged');
|
|
365
|
+
/** 同一 turn 的续步没有新真人消息:不重新问人,但盘上变了必须换掉快照。 */
|
|
366
|
+
await bootA.preStep({ agent, signal: sig, turn: 't1' }, enter());
|
|
367
|
+
const after = bootA.provider.text({ agent, signal: sig });
|
|
368
|
+
assert.doesNotMatch(after, /全局发布前文案核对/);
|
|
369
|
+
assert.match(after, /发布前跑文案检查/, '项目层卡片仍在');
|
|
370
|
+
const sigNext = signal();
|
|
371
|
+
await bootA.preStep({ agent, signal: sigNext, turn: 't2' }, enter(human('大正文样本怎么用')));
|
|
372
|
+
assert.match(bootA.debug.preparedOf(agent).query, /大正文样本/);
|
|
373
|
+
assert.doesNotMatch(bootA.provider.text({ agent, signal: sigNext }), /全局发布前文案核对/);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// A17:工具模式不自动注入,但工具调用同样共享每请求额度。
|
|
377
|
+
{
|
|
378
|
+
const toolOnly = boot('tool_only');
|
|
379
|
+
const agent = makeAgent('agent-tool-budget', cwdA);
|
|
380
|
+
await prepare(toolOnly, agent, '大正文样本怎么用');
|
|
381
|
+
assert.equal(toolOnly.debug.preparedOf(agent), null);
|
|
382
|
+
const win = toolOnly.debug.windowOf(agent);
|
|
383
|
+
assert.ok(win, '工具模式也开额度窗口');
|
|
384
|
+
let exhausted = null;
|
|
385
|
+
for (let i = 0; i < 9 && !exhausted; i += 1) {
|
|
386
|
+
const row = await toolOnly.exec('memory_read', { id: bigBodies[i].id }, agent);
|
|
387
|
+
if (row.status === 'budget_exhausted') exhausted = row;
|
|
388
|
+
}
|
|
389
|
+
assert.ok(exhausted, '超过额度后拒绝读正文');
|
|
390
|
+
assert.equal(exhausted.id, '');
|
|
391
|
+
assert.ok(win.bytes <= 8192);
|
|
392
|
+
assert.ok(win.bodies <= 3);
|
|
393
|
+
assert.ok(win.reads <= 8);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// A18:归档/旧正文不进正常检索;只有明确查历史才列归档。
|
|
397
|
+
{
|
|
398
|
+
const cwdE = mkdir('e');
|
|
399
|
+
const scopeE = store.scopeOf(cwdE);
|
|
400
|
+
const archivedEntry = save(scopeE, { title: '旧端口修正', tags: ['端口'], summary: '端口改到 49661', body: '旧正文:端口是 49661' });
|
|
401
|
+
const agent = makeAgent('agent-history', cwdE);
|
|
402
|
+
const fresh = await bootA.exec('memory_search', { query: '旧端口修正' }, agent);
|
|
403
|
+
assert.equal(fresh.count, 1);
|
|
404
|
+
await bootA.exec('memory_forget', { id: archivedEntry.id, mode: 'archive' }, agent);
|
|
405
|
+
const hidden = await bootA.exec('memory_search', { query: '旧端口修正' }, agent);
|
|
406
|
+
assert.equal(hidden.count, 0, '归档后默认检索不到');
|
|
407
|
+
const history = await bootA.exec('memory_search', { query: '旧端口修正', history: true }, agent);
|
|
408
|
+
assert.equal(history.count, 1);
|
|
409
|
+
assert.equal(history.refs[0].status, 'archived');
|
|
410
|
+
const oldBody = save(scopeE, { title: '口端口令', tags: ['端口'], summary: '端口以最新为准', body: '第一版:端口是 5000' });
|
|
411
|
+
store.upsert(scopeE, { id: oldBody.id, title: '口端口令', summary: '端口以最新为准', body: '第二版:端口是 49661' });
|
|
412
|
+
const current = await bootA.exec('memory_search', { query: '口端口令', history: true }, agent);
|
|
413
|
+
assert.ok(current.refs.some((ref) => ref.id === oldBody.id && ref.revision === 2), '检索只给当前版本');
|
|
414
|
+
const latest = await bootA.exec('memory_read', { id: oldBody.id }, agent);
|
|
415
|
+
assert.match(latest.body, /第二版/);
|
|
416
|
+
assert.doesNotMatch(latest.body, /第一版/);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// A19:预算按规范化 JSON 字节算,中文、换行、引号、反斜杠都算进去。
|
|
420
|
+
{
|
|
421
|
+
const cwdF = mkdir('f');
|
|
422
|
+
const scopeF = store.scopeOf(cwdF);
|
|
423
|
+
const tricky = store.upsert(scopeF, {
|
|
424
|
+
title: '转义预算样本',
|
|
425
|
+
summary: '换行与引号',
|
|
426
|
+
applicability: 'all_in_scope',
|
|
427
|
+
body: '第一行\n第二行 "双引号" \\反斜杠\t制表',
|
|
428
|
+
});
|
|
429
|
+
const agent = makeAgent('agent-json-budget', cwdF);
|
|
430
|
+
await bootA.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('转义预算样本怎么读')));
|
|
431
|
+
const win = bootA.debug.windowOf(agent);
|
|
432
|
+
const before = win.bytes;
|
|
433
|
+
const value = await bootA.exec('memory_read', { id: tricky.record.id }, agent);
|
|
434
|
+
assert.equal(value.status, 'ok');
|
|
435
|
+
assert.equal(win.bytes - before, Buffer.byteLength(JSON.stringify(value)));
|
|
436
|
+
assert.ok(win.bytes <= 8192);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// A20:没有命中是正常空结果,不是额度用尽。
|
|
440
|
+
{
|
|
441
|
+
const agent = makeAgent('agent-empty-search', cwdA);
|
|
442
|
+
await bootA.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('完全无关的一串词')));
|
|
443
|
+
const value = await bootA.exec('memory_search', { query: '完全不存在的主题 zzzqqq' }, agent);
|
|
444
|
+
assert.equal(value.status, 'ok');
|
|
445
|
+
assert.equal(value.count, 0);
|
|
446
|
+
assert.deepEqual(value.refs, []);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log('recall.test.mjs: ok');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三个视频使用场景 + 一组新会话对照。
|
|
3
|
+
* 只验证确定性边界:规则是否按条件出现、样例/检查点是否有限、关闭后是否停止。
|
|
4
|
+
* 这里不调用模型,因此不产出“更聪明”的效果结论。
|
|
5
|
+
*/
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-scenario-'));
|
|
12
|
+
const store = await import('../store.js');
|
|
13
|
+
const learning = await import('../learning.js');
|
|
14
|
+
const anchor = await import('../anchor.js');
|
|
15
|
+
const view = await import('../view.js');
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
const test = (name, fn) => {
|
|
19
|
+
try {
|
|
20
|
+
fn();
|
|
21
|
+
passed += 1;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(`FAIL ${name}`);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const fresh = (label) => {
|
|
29
|
+
const dir = path.join(os.tmpdir(), `lite-memory-scenario-${label}-${Math.random().toString(16).slice(2, 8)}`);
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
return store.scopeOf(dir);
|
|
32
|
+
};
|
|
33
|
+
const ruleRows = (scope) => store.loadIndex(scope).entries.filter((row) => row.origin === 'project_evolution');
|
|
34
|
+
const ruleOf = (scope, id) => ruleRows(scope).find((row) => row.id === id);
|
|
35
|
+
const enable = (scope, label) => {
|
|
36
|
+
const result = store.setEvolution(scope, true, { operationId: `on_${label}` });
|
|
37
|
+
assert.equal(result.ok, true, result.reason);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function seedEpisode(scope, { sessionId, messageIds, query, defaults }) {
|
|
41
|
+
const routed = anchor.routeTask(store.taskState(scope), {
|
|
42
|
+
sessionId,
|
|
43
|
+
projectId: 'proj',
|
|
44
|
+
query,
|
|
45
|
+
messageIds,
|
|
46
|
+
batchKey: anchor.requestBatchKey(messageIds),
|
|
47
|
+
defaults,
|
|
48
|
+
now: Date.now(),
|
|
49
|
+
});
|
|
50
|
+
const saved = store.saveTaskState(scope, routed.tasks, { operationId: `tasks_${sessionId}_${Date.now()}` });
|
|
51
|
+
assert.equal(saved.ok, true, saved.reason);
|
|
52
|
+
return { episode: routed.episode, sessionId, messageIds };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function begin(scope, grown) {
|
|
56
|
+
const result = store.beginLearningAttempt(scope, { episodeId: grown.episode.episodeId, operationId: `start_${Math.random().toString(16).slice(2, 10)}` });
|
|
57
|
+
assert.equal(result.ok, true, result.reason);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const RANGES = { taskKinds: ['video'], series: ['口播', '剪辑', '字幕'] };
|
|
62
|
+
|
|
63
|
+
function parse(change, rules = []) {
|
|
64
|
+
const refs = { feedback: { u0: 'm1' }, rules: Object.fromEntries(rules.map((rule) => [rule.ref, rule.id])) };
|
|
65
|
+
return learning.parseOutput(JSON.stringify({ changes: [change] }), { finish: { kind: 'stop' }, refs, rules, ranges: RANGES });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function commitNew(scope, grown, started, change) {
|
|
69
|
+
const check = parse(change);
|
|
70
|
+
assert.equal(check.ok, true, check.reason);
|
|
71
|
+
return store.commitLearning(scope, {
|
|
72
|
+
operationId: `learn_${Math.random().toString(16).slice(2, 10)}`,
|
|
73
|
+
epoch: started.epoch,
|
|
74
|
+
episodeId: grown.episode.episodeId,
|
|
75
|
+
episodeUpdatedAt: grown.episode.updatedAt,
|
|
76
|
+
projectId: 'proj',
|
|
77
|
+
sessionId: grown.sessionId,
|
|
78
|
+
messageIds: grown.messageIds,
|
|
79
|
+
changes: check.changes,
|
|
80
|
+
preparedTargets: [],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function commitExisting(scope, grown, started, id, change, feedbackText = '') {
|
|
85
|
+
const flat = ruleOf(scope, id);
|
|
86
|
+
const rules = [{ ref: 'r0', id, revision: flat.revision, conditions: flat.conditions }];
|
|
87
|
+
const check = parse(change, rules);
|
|
88
|
+
assert.equal(check.ok, true, check.reason);
|
|
89
|
+
return store.commitLearning(scope, {
|
|
90
|
+
operationId: `learn_${Math.random().toString(16).slice(2, 10)}`,
|
|
91
|
+
epoch: started.epoch,
|
|
92
|
+
episodeId: grown.episode.episodeId,
|
|
93
|
+
episodeUpdatedAt: grown.episode.updatedAt,
|
|
94
|
+
projectId: 'proj',
|
|
95
|
+
sessionId: grown.sessionId,
|
|
96
|
+
messageIds: grown.messageIds,
|
|
97
|
+
feedbackText: feedbackText || change.currentText || '',
|
|
98
|
+
changes: check.changes,
|
|
99
|
+
preparedTargets: [{ id, revision: flat.revision, specDigest: flat.learning.specDigest }],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const NEW_VIDEO_RULE = (topicKey, text, seriesId) => ({
|
|
104
|
+
target: 'new',
|
|
105
|
+
feedbackType: 'spec_change',
|
|
106
|
+
topicKey,
|
|
107
|
+
currentText: text,
|
|
108
|
+
applicability: 'conditional',
|
|
109
|
+
conditions: [{ key: 'seriesId', value: seriesId }],
|
|
110
|
+
evidence: ['u0'],
|
|
111
|
+
persistence: 'project_candidate',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('S1 口播:规范生效,执行漏掉只补检查点,新会话仍读当前正文', () => {
|
|
115
|
+
const scope = fresh('koubo');
|
|
116
|
+
enable(scope, 'koubo');
|
|
117
|
+
const grown = seedEpisode(scope, { sessionId: 's-koubo', messageIds: ['m1'], query: '以后口播开头都先给结论', defaults: { taskKind: 'video', seriesId: '口播' } });
|
|
118
|
+
assert.equal(commitNew(scope, grown, begin(scope, grown), NEW_VIDEO_RULE('口播开头', '开头三句内给结论。', '口播')).ok, true);
|
|
119
|
+
const id = ruleRows(scope)[0].id;
|
|
120
|
+
const before = ruleOf(scope, id);
|
|
121
|
+
const miss = { target: 'r0', feedbackType: 'execution_miss', currentText: '交付前先核对开头三句内有没有结论。', evidence: ['u0'], persistence: 'project_candidate' };
|
|
122
|
+
assert.equal(commitExisting(scope, grown, begin(scope, grown), id, miss).ok, true);
|
|
123
|
+
const after = ruleOf(scope, id);
|
|
124
|
+
assert.equal(after.body, before.body);
|
|
125
|
+
assert.equal(after.learning.state, 'trial');
|
|
126
|
+
assert.equal(after.learning.checkpoints.length, 1);
|
|
127
|
+
assert.equal((after.learning.samples || []).length, 0);
|
|
128
|
+
assert.equal(view.evaluate(after, { taskKind: 'video', seriesId: '口播' }).state, 'match');
|
|
129
|
+
assert.equal(view.evaluate(after, { taskKind: 'video', seriesId: '字幕' }).state, 'mismatch');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('S2 剪辑:认可只留一条带版本和维度的样例,不改正文也不抬状态', () => {
|
|
133
|
+
const scope = fresh('jianji');
|
|
134
|
+
enable(scope, 'jianji');
|
|
135
|
+
const grown = seedEpisode(scope, { sessionId: 's-jianji', messageIds: ['m1'], query: '以后剪辑都先铺节奏再切', defaults: { taskKind: 'video', seriesId: '剪辑' } });
|
|
136
|
+
assert.equal(commitNew(scope, grown, begin(scope, grown), NEW_VIDEO_RULE('剪辑节奏', '先铺节奏,再切镜头。', '剪辑')).ok, true);
|
|
137
|
+
const id = ruleRows(scope)[0].id;
|
|
138
|
+
const before = ruleOf(scope, id);
|
|
139
|
+
const accept = { target: 'r0', feedbackType: 'artifact_accept', evidence: ['u0'], persistence: 'project_candidate' };
|
|
140
|
+
assert.equal(commitExisting(scope, grown, begin(scope, grown), id, accept, '这版剪辑的节奏可以').ok, true);
|
|
141
|
+
const after = ruleOf(scope, id);
|
|
142
|
+
assert.equal(after.body, before.body);
|
|
143
|
+
assert.equal(after.learning.state, before.learning.state);
|
|
144
|
+
assert.equal((after.learning.samples || []).length, 1);
|
|
145
|
+
assert.equal(after.learning.samples[0].dimension, '节奏');
|
|
146
|
+
assert.equal(after.learning.samples[0].verified, false);
|
|
147
|
+
assert.ok(after.learning.samples[0].artifactVersion);
|
|
148
|
+
assert.equal(view.evaluate(after, { taskKind: 'video', seriesId: '剪辑' }).state, 'match');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('S3 字幕:一次性例外不写成规范,也不把口播规则带进来', () => {
|
|
152
|
+
const scope = fresh('zimu');
|
|
153
|
+
enable(scope, 'zimu');
|
|
154
|
+
const grown = seedEpisode(scope, { sessionId: 's-zimu', messageIds: ['m1'], query: '以后口播开头都先给结论', defaults: { taskKind: 'video', seriesId: '口播' } });
|
|
155
|
+
assert.equal(commitNew(scope, grown, begin(scope, grown), NEW_VIDEO_RULE('口播开头', '开头三句内给结论。', '口播')).ok, true);
|
|
156
|
+
const exception = { target: 'new', feedbackType: 'task_exception', topicKey: '字幕例外', currentText: '这期字幕晚一点出现。', evidence: ['u0'], persistence: 'project_candidate' };
|
|
157
|
+
const check = parse(exception);
|
|
158
|
+
assert.equal(check.ok, false);
|
|
159
|
+
assert.match(check.reason, /不直接形成长期规则/);
|
|
160
|
+
assert.equal(ruleRows(scope).length, 1);
|
|
161
|
+
assert.equal(view.evaluate(ruleRows(scope)[0], { taskKind: 'video', seriesId: '字幕' }).state, 'mismatch');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('S4 新会话对照:有规则才命中;关闭后不再读,未知用量保持未知', () => {
|
|
165
|
+
const learned = fresh('contrast-on');
|
|
166
|
+
const blank = fresh('contrast-off');
|
|
167
|
+
enable(learned, 'contrast');
|
|
168
|
+
const grown = seedEpisode(learned, { sessionId: 's-on', messageIds: ['m1'], query: '以后口播开头都先给结论', defaults: { taskKind: 'video', seriesId: '口播' } });
|
|
169
|
+
assert.equal(commitNew(learned, grown, begin(learned, grown), NEW_VIDEO_RULE('口播开头', '开头三句内给结论。', '口播')).ok, true);
|
|
170
|
+
const context = { taskKind: 'video', seriesId: '口播' };
|
|
171
|
+
assert.equal(ruleRows(learned).filter((row) => view.canAutoRecall(row, context)).length, 1);
|
|
172
|
+
assert.equal(ruleRows(blank).filter((row) => view.canAutoRecall(row, context)).length, 0);
|
|
173
|
+
const onView = view.toModelView(ruleRows(learned)[0], 'read', { context });
|
|
174
|
+
assert.equal(onView.body, '开头三句内给结论。');
|
|
175
|
+
assert.ok(Buffer.byteLength(JSON.stringify(onView)) <= 1024);
|
|
176
|
+
store.setEvolution(learned, false, { operationId: 'off_contrast' });
|
|
177
|
+
const snapshot = store.learningSnapshot(learned);
|
|
178
|
+
assert.equal(snapshot.enabled, false);
|
|
179
|
+
assert.equal(snapshot.rules[0].state, 'paused');
|
|
180
|
+
assert.equal(snapshot.effect.inputTokens, null);
|
|
181
|
+
assert.equal(store.beginLearningAttempt(learned, { episodeId: grown.episode.episodeId, operationId: 'start_off' }).ok, false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(`scenario.test.mjs: ${passed} passed`);
|