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.
- package/README.md +44 -18
- package/anchor.js +152 -13
- package/client.js +1083 -120
- 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/docs/RELEASE-0.16.0.md +10 -0
- package/index.js +704 -102
- package/learning.js +58 -32
- package/package.json +2 -2
- package/state.js +112 -11
- package/store.js +343 -44
- package/test/anchor.test.mjs +311 -0
- package/test/client.test.mjs +48 -6
- package/test/contract.test.mjs +214 -11
- package/test/control.test.mjs +297 -0
- package/test/evolution-read.test.mjs +196 -11
- package/test/learning.test.mjs +4 -2
- package/test/lexical.test.mjs +33 -0
- package/test/recall.test.mjs +25 -9
- package/test/state.test.mjs +21 -0
- package/test/store.test.mjs +98 -8
- package/view.js +16 -1
package/test/contract.test.mjs
CHANGED
|
@@ -108,13 +108,13 @@ assert.equal(phrased.scope, 'project');
|
|
|
108
108
|
assert.equal(store.loadIndex(store.globalScope()).entries.some((item) => item.title === '以后都用这个一次性脚本'), false);
|
|
109
109
|
|
|
110
110
|
assert.equal(typeof saved.id, 'string');
|
|
111
|
-
assert.match(render('memory_save', {}, saved), /^memory · saved "发布顺序" · mem_[a-z0-9]
|
|
111
|
+
assert.match(render('memory_save', {}, saved), /^memory · saved \[[^\]]+\] "发布顺序" · mem_[a-z0-9]+$/);
|
|
112
112
|
|
|
113
113
|
const merged = await exec('memory_save', { title: '发布顺序', summary: '发版前要按顺序做的事', body: '第一步\n第二步' });
|
|
114
114
|
assert.equal(merged.merged, true);
|
|
115
115
|
assert.equal(merged.id, saved.id);
|
|
116
116
|
assert.equal(merged.revision, 2);
|
|
117
|
-
assert.match(render('memory_save', {}, merged), /^memory · merged "发布顺序" · mem_[a-z0-9]
|
|
117
|
+
assert.match(render('memory_save', {}, merged), /^memory · merged \[[^\]]+\] "发布顺序" · mem_[a-z0-9]+$/);
|
|
118
118
|
|
|
119
119
|
const patch = await exec('memory_save', { id: saved.id, title: '发布顺序', summary: '发版前要按顺序做的事', when: '发版当天' });
|
|
120
120
|
assert.equal(patch.when, '发版当天');
|
|
@@ -125,7 +125,7 @@ const again = await exec('memory_save', { title: '发布顺序', summary: '发
|
|
|
125
125
|
assert.equal(again.changed, false);
|
|
126
126
|
assert.equal(again.id, saved.id);
|
|
127
127
|
assert.equal(again.revision, 3);
|
|
128
|
-
assert.match(render('memory_save', {}, again), /^memory · unchanged "发布顺序" · mem_[a-z0-9]
|
|
128
|
+
assert.match(render('memory_save', {}, again), /^memory · unchanged \[[^\]]+\] "发布顺序" · mem_[a-z0-9]+$/);
|
|
129
129
|
|
|
130
130
|
// ── similar title asks before overwriting ────────────────────────────────────
|
|
131
131
|
const probe = await exec('memory_save', { title: '发布顺序是啥', summary: '发版前要按顺序做的事', body: '另一条' });
|
|
@@ -147,16 +147,20 @@ const hits = await exec('memory_search', { query: '发布流程' });
|
|
|
147
147
|
assert.ok(hits.count >= 1);
|
|
148
148
|
assert.ok(hits.chars > 0);
|
|
149
149
|
const hitText = render('memory_search', {}, hits);
|
|
150
|
-
assert.match(hitText, new RegExp(`^memory · 命中 ${hits.count} 条 · 引用 ${hits.chars} 字\\n- mem_`));
|
|
151
|
-
assert.
|
|
150
|
+
assert.match(hitText, new RegExp(`^memory · 命中 ${hits.count} 条 · 引用 ${hits.chars} 字\\n- \\[[^\\]]+\\] mem_`));
|
|
151
|
+
assert.doesNotMatch(hitText, /第 .* 版/);
|
|
152
|
+
/** 「发版当天」这类自由文本条件没有可信上下文,不能当成适用条目塞给模型。 */
|
|
152
153
|
const miss = await exec('memory_search', { query: '完全没有出现过的主题' });
|
|
153
154
|
assert.equal(miss.count, 0);
|
|
154
155
|
assert.equal(render('memory_search', {}, miss), 'memory · 没有命中');
|
|
155
156
|
|
|
156
157
|
const read = await exec('memory_read', { id: saved.id });
|
|
157
|
-
assert.equal(read.
|
|
158
|
-
assert.equal(read.
|
|
159
|
-
|
|
158
|
+
assert.equal(read.status, 'needs_context');
|
|
159
|
+
assert.equal(read.body, '');
|
|
160
|
+
const plain = await exec('memory_read', { id: distinct.id });
|
|
161
|
+
assert.equal(plain.body, '另一条');
|
|
162
|
+
assert.equal(plain.revision, 1);
|
|
163
|
+
assert.match(render('memory_read', {}, plain), new RegExp(`^memory · \\[[^\\]]+\\] 「发布顺序是啥」 · ${distinct.id}`));
|
|
160
164
|
|
|
161
165
|
// 没点明归档还是彻底忘掉时不动盘,先返回待确认
|
|
162
166
|
const undecided = await exec('memory_forget', { id: distinct.id });
|
|
@@ -165,7 +169,7 @@ assert.match(render('memory_forget', {}, undecided), new RegExp(`^memory · 先
|
|
|
165
169
|
assert.ok(store.locate(store.scopeOf(cwd), distinct.id));
|
|
166
170
|
const archived = await exec('memory_forget', { id: distinct.id, mode: 'archive' });
|
|
167
171
|
assert.equal(archived.id, distinct.id);
|
|
168
|
-
assert.match(render('memory_forget', {}, archived), new RegExp(`^memory · archived "发布顺序是啥" · ${distinct.id},可找回$`));
|
|
172
|
+
assert.match(render('memory_forget', {}, archived), new RegExp(`^memory · archived \\[[^\\]]+\\] "发布顺序是啥" · ${distinct.id},可找回$`));
|
|
169
173
|
assert.equal(await exec('memory_save', { title: '发布顺序是啥', summary: '发版前要按顺序做的事', body: '第三条' }).then((v) => v.merged), false);
|
|
170
174
|
|
|
171
175
|
// ── suggestion → keep → replay → reject ──────────────────────────────────────
|
|
@@ -227,10 +231,10 @@ assert.equal((await call('/lite-memory/keep', {}, { method: 'GET' })).status, 40
|
|
|
227
231
|
// ── global layer ─────────────────────────────────────────────────────────────
|
|
228
232
|
const global = await exec('memory_save', { title: '回复用中文', summary: '用户要求回复用中文', scope: 'global' });
|
|
229
233
|
assert.equal(global.scope, 'global');
|
|
230
|
-
assert.match(render('memory_save', {}, global), /^memory · saved \[
|
|
234
|
+
assert.match(render('memory_save', {}, global), /^memory · saved \[global\] "回复用中文" · mem_[a-z0-9]+$/);
|
|
231
235
|
const globalSearch = await exec('memory_search', { query: '回复用中文' });
|
|
232
236
|
assert.equal(globalSearch.count, 1);
|
|
233
|
-
assert.match(render('memory_search', {}, globalSearch), /\[
|
|
237
|
+
assert.match(render('memory_search', {}, globalSearch), /\[global\] mem_/);
|
|
234
238
|
assert.equal(typeof globalThis.__liteMemoryStats, 'function');
|
|
235
239
|
const stats = globalThis.__liteMemoryStats(cwd);
|
|
236
240
|
assert.ok(stats.count >= 1);
|
|
@@ -415,6 +419,102 @@ const evoDelete = await call('/lite-memory/evolution', {
|
|
|
415
419
|
assert.equal(evoDelete.status, 200);
|
|
416
420
|
assert.equal(evoDelete.body.ok, true);
|
|
417
421
|
|
|
422
|
+
// 管理面成功路径:候选可修改、采纳、暂停、恢复、放弃,每次返回后能继续操作
|
|
423
|
+
const manageSeed = await defs.get('memory_save').execute(
|
|
424
|
+
{ title: '口播收尾', summary: '口播收尾留一句行动', body: '结尾给一句具体行动。' },
|
|
425
|
+
{ agent: evoAgent },
|
|
426
|
+
);
|
|
427
|
+
state.transact(evoReadScope, {}, (draft) => {
|
|
428
|
+
const row = draft.entries.find((entry) => entry.id === manageSeed.id);
|
|
429
|
+
row.origin = 'project_evolution';
|
|
430
|
+
row.current.conditions = [{ key: 'seriesId', value: '口播' }];
|
|
431
|
+
row.maintenance = { ...(row.maintenance || {}), learning: { state: 'pending', source: 'evolution' } };
|
|
432
|
+
return { changed: true };
|
|
433
|
+
});
|
|
434
|
+
const manageSnap = await call('/lite-memory/evolution', { sessionId: 'session-evo-read', learningAction: 'snapshot' });
|
|
435
|
+
const manageRule = manageSnap.body.rules.find((rule) => rule.id === manageSeed.id);
|
|
436
|
+
assert.ok(manageRule);
|
|
437
|
+
assert.equal(manageRule.state, 'pending');
|
|
438
|
+
const manageEdit = await call('/lite-memory/evolution', {
|
|
439
|
+
sessionId: 'session-evo-read', learningAction: 'edit', id: manageSeed.id,
|
|
440
|
+
expectedRevision: manageRule.revision, body: '结尾给一句具体动作。',
|
|
441
|
+
});
|
|
442
|
+
assert.equal(manageEdit.status, 200);
|
|
443
|
+
assert.equal(manageEdit.body.ok, true);
|
|
444
|
+
const afterManageEdit = (await call('/lite-memory/evolution', { sessionId: 'session-evo-read', learningAction: 'snapshot' })).body.rules.find((rule) => rule.id === manageSeed.id);
|
|
445
|
+
assert.equal(afterManageEdit.state, 'pending');
|
|
446
|
+
assert.equal(afterManageEdit.body, '结尾给一句具体动作。');
|
|
447
|
+
const manageAdopt = await call('/lite-memory/evolution', {
|
|
448
|
+
sessionId: 'session-evo-read', learningAction: 'adopt', id: manageSeed.id, expectedRevision: afterManageEdit.revision,
|
|
449
|
+
});
|
|
450
|
+
assert.equal(manageAdopt.status, 200);
|
|
451
|
+
assert.equal((await call('/lite-memory/evolution', { sessionId: 'session-evo-read', learningAction: 'snapshot' })).body.rules.find((rule) => rule.id === manageSeed.id).state, 'active');
|
|
452
|
+
const managePause = await call('/lite-memory/evolution', {
|
|
453
|
+
sessionId: 'session-evo-read', learningAction: 'pause', ids: [manageSeed.id],
|
|
454
|
+
});
|
|
455
|
+
assert.equal(managePause.status, 200);
|
|
456
|
+
assert.equal((await call('/lite-memory/evolution', { sessionId: 'session-evo-read', learningAction: 'snapshot' })).body.rules.find((rule) => rule.id === manageSeed.id).state, 'paused');
|
|
457
|
+
const manageResume = await call('/lite-memory/evolution', {
|
|
458
|
+
sessionId: 'session-evo-read', learningAction: 'resume', id: manageSeed.id,
|
|
459
|
+
});
|
|
460
|
+
assert.equal(manageResume.status, 200);
|
|
461
|
+
const manageResumed = (await call('/lite-memory/evolution', { sessionId: 'session-evo-read', learningAction: 'snapshot' })).body.rules.find((rule) => rule.id === manageSeed.id);
|
|
462
|
+
assert.equal(manageResumed.state, 'trial');
|
|
463
|
+
const manageDiscard = await call('/lite-memory/evolution', {
|
|
464
|
+
sessionId: 'session-evo-read', learningAction: 'discard', id: manageSeed.id, expectedRevision: manageResumed.revision,
|
|
465
|
+
});
|
|
466
|
+
assert.equal(manageDiscard.status, 200);
|
|
467
|
+
assert.equal((await call('/lite-memory/evolution', { sessionId: 'session-evo-read', learningAction: 'snapshot' })).body.rules.length, 0);
|
|
468
|
+
|
|
469
|
+
// ── 关闭项目记忆后:面板只读,管理面写操作全部拒绝,工具也不落盘 ─────────────
|
|
470
|
+
const closedCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-closed-'));
|
|
471
|
+
sessionCwds.set('session-closed', closedCwd);
|
|
472
|
+
const closedScope = store.scopeOf(closedCwd);
|
|
473
|
+
const closedAgent = { id: 'session-closed', cwd: closedCwd, session: { id: 'session-closed', header: { cwd: closedCwd } } };
|
|
474
|
+
const closedSeed = await defs.get('memory_save').execute(
|
|
475
|
+
{ title: '口播开头', summary: '口播开头三句内给结论', body: '开场三句内给结论。' },
|
|
476
|
+
{ agent: closedAgent },
|
|
477
|
+
);
|
|
478
|
+
state.transact(closedScope, {}, (draft) => {
|
|
479
|
+
const row = draft.entries.find((entry) => entry.id === closedSeed.id);
|
|
480
|
+
row.origin = 'project_evolution';
|
|
481
|
+
row.maintenance = { ...(row.maintenance || {}), learning: { state: 'active' } };
|
|
482
|
+
return { changed: true };
|
|
483
|
+
});
|
|
484
|
+
assert.equal((await call('/lite-memory/evolution', { sessionId: 'session-closed', enabled: true })).status, 200);
|
|
485
|
+
const closedOff = await call('/lite-memory/control', { sessionId: 'session-closed', enabled: false, operationId: 'op_close_contract' });
|
|
486
|
+
assert.equal(closedOff.status, 200);
|
|
487
|
+
assert.equal(closedOff.body.enabled, false);
|
|
488
|
+
const closedFile = path.join(closedScope.dir, 'state.json');
|
|
489
|
+
const closedBefore = fs.readFileSync(closedFile);
|
|
490
|
+
const closedSnapshot = await call('/lite-memory/evolution', { sessionId: 'session-closed', learningAction: 'snapshot' });
|
|
491
|
+
assert.equal(closedSnapshot.status, 200);
|
|
492
|
+
assert.equal(closedSnapshot.body.enabled, false);
|
|
493
|
+
assert.equal(closedSnapshot.body.rules.length, 1);
|
|
494
|
+
assert.equal(Buffer.compare(closedBefore, fs.readFileSync(closedFile)), 0);
|
|
495
|
+
const closedRevision = closedSnapshot.body.rules[0].revision;
|
|
496
|
+
for (const action of ['adopt', 'edit', 'delete', 'pause', 'resume', 'pin', 'undo']) {
|
|
497
|
+
const payload = { sessionId: 'session-closed', learningAction: action, id: closedSeed.id, expectedRevision: closedRevision, body: '改过的正文' };
|
|
498
|
+
if (action === 'pin') payload.pinned = true;
|
|
499
|
+
if (action === 'pause') payload.ids = [closedSeed.id];
|
|
500
|
+
const result = await call('/lite-memory/evolution', payload);
|
|
501
|
+
assert.equal(result.status, 409, action);
|
|
502
|
+
assert.equal(result.body.code, 'memory_disabled', action);
|
|
503
|
+
}
|
|
504
|
+
const closedTool = await defs.get('memory_save').execute(
|
|
505
|
+
{ title: '关闭后写入', summary: '应当拒绝', body: '不该落盘' },
|
|
506
|
+
{ agent: closedAgent },
|
|
507
|
+
);
|
|
508
|
+
assert.equal(closedTool.changed, false);
|
|
509
|
+
assert.equal(closedTool.reason, 'memory_disabled');
|
|
510
|
+
assert.equal(closedTool.id, '');
|
|
511
|
+
assert.equal(Buffer.compare(closedBefore, fs.readFileSync(closedFile)), 0);
|
|
512
|
+
const reopened = await call('/lite-memory/control', { sessionId: 'session-closed', enabled: true, operationId: 'op_reopen_contract' });
|
|
513
|
+
assert.equal(reopened.status, 200);
|
|
514
|
+
const reopenedSnapshot = await call('/lite-memory/evolution', { sessionId: 'session-closed', learningAction: 'snapshot' });
|
|
515
|
+
assert.equal(reopenedSnapshot.body.enabled, false);
|
|
516
|
+
assert.equal(reopenedSnapshot.body.rules.length, 1);
|
|
517
|
+
|
|
418
518
|
// ── 界面只吃持久元数据,不猜文案 ─────────────────────────────────────────────
|
|
419
519
|
const metaOf = (name, args, value) => defs.get(name).output.presentationMeta(args, value);
|
|
420
520
|
const savedMeta = metaOf('memory_save', {}, saved);
|
|
@@ -674,4 +774,107 @@ assert.equal(orgEconomy2.knownCalls, 3);
|
|
|
674
774
|
assert.equal(orgEconomy2.inputTokens, 36);
|
|
675
775
|
llmImpl = null;
|
|
676
776
|
|
|
777
|
+
// ── 记忆面板总览:只读枚举各项目层,正文按 scopeKey 单读,不泄露内部路径 ──────
|
|
778
|
+
const ovCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-overview-'));
|
|
779
|
+
const ovOtherCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-overview-other-'));
|
|
780
|
+
sessionCwds.set('session-overview', ovCwd);
|
|
781
|
+
sessionCwds.set('session-overview-other', ovOtherCwd);
|
|
782
|
+
const ovAgent = { id: 'session-overview', cwd: ovCwd, session: { id: 'session-overview', header: { cwd: ovCwd } } };
|
|
783
|
+
const ovFirst = await defs.get('memory_save').execute(
|
|
784
|
+
{ title: '开场先给结论', summary: '口播开头先给结论', body: '开场三句内给结论。', tags: ['口播', '开头'], links: [] },
|
|
785
|
+
{ agent: ovAgent },
|
|
786
|
+
);
|
|
787
|
+
const ovSecond = await defs.get('memory_save').execute(
|
|
788
|
+
{ title: '封面留白', summary: '封面别放满', body: '右侧留白。', tags: ['封面'] },
|
|
789
|
+
{ agent: ovAgent },
|
|
790
|
+
);
|
|
791
|
+
await defs.get('memory_save').execute(
|
|
792
|
+
{ title: '观察全局', summary: '跨项目通用说明', body: '全局正文。', scope: 'global' },
|
|
793
|
+
{ agent: ovAgent },
|
|
794
|
+
);
|
|
795
|
+
const ovOther = await defs.get('memory_save').execute(
|
|
796
|
+
{ title: '别的项目', summary: '只在别的项目里', body: '另一层的正文。', tags: ['别的'] },
|
|
797
|
+
{ agent: { id: 'session-overview-other', cwd: ovOtherCwd, session: { id: 'session-overview-other', header: { cwd: ovOtherCwd } } } },
|
|
798
|
+
);
|
|
799
|
+
const ov = await call('/lite-memory/overview', { sessionId: 'session-overview' });
|
|
800
|
+
assert.equal(ov.status, 200);
|
|
801
|
+
assert.equal(ov.body.ok, true);
|
|
802
|
+
assert.equal(ov.body.current.key, store.scopeOf(ovCwd).key);
|
|
803
|
+
assert.equal(ov.body.current.label, path.basename(ovCwd));
|
|
804
|
+
assert.equal(ov.body.current.evolution.enabled, false);
|
|
805
|
+
assert.equal(ov.body.selected.key, ov.body.current.key);
|
|
806
|
+
assert.equal(ov.body.selected.kind, 'project');
|
|
807
|
+
assert.equal(ov.body.selected.evolution.enabled, false);
|
|
808
|
+
assert.ok(Array.isArray(ov.body.selected.evolution.rules));
|
|
809
|
+
assert.ok(ov.body.selected.records.some((row) => row.id === ovFirst.id && row.title === '开场先给结论'));
|
|
810
|
+
assert.equal(ov.body.selected.records.some((row) => 'body' in row), false, '列表不带正文');
|
|
811
|
+
assert.equal(ov.body.selected.stats.total, 2);
|
|
812
|
+
assert.equal(ov.body.selected.stats.tags, 3);
|
|
813
|
+
assert.equal(JSON.stringify(ov.body).includes('"dir"'), false, '不暴露内部目录');
|
|
814
|
+
assert.equal(JSON.stringify(ov.body).includes(process.env.DSH_HOME), false);
|
|
815
|
+
const ovKeys = ov.body.scopes.map((row) => row.key);
|
|
816
|
+
assert.ok(ovKeys.includes(ov.body.current.key));
|
|
817
|
+
assert.ok(ovKeys.includes(store.scopeOf(ovOtherCwd).key));
|
|
818
|
+
assert.ok(ovKeys.includes('global'), '全局层总在列表里');
|
|
819
|
+
|
|
820
|
+
// 切到别的项目只是换一面看,不写那个项目
|
|
821
|
+
const ovOtherView = await call('/lite-memory/overview', { sessionId: 'session-overview', scopeKey: store.scopeOf(ovOtherCwd).key });
|
|
822
|
+
assert.equal(ovOtherView.status, 200);
|
|
823
|
+
assert.equal(ovOtherView.body.selected.key, store.scopeOf(ovOtherCwd).key);
|
|
824
|
+
assert.deepEqual(ovOtherView.body.selected.records.map((row) => row.title), ['别的项目']);
|
|
825
|
+
assert.equal(ovOtherView.body.selected.evolution.enabled, false, '别的项目也要给只读实验状态');
|
|
826
|
+
assert.equal(ovOtherView.body.current.key, ov.body.current.key);
|
|
827
|
+
const ovGlobal = await call('/lite-memory/overview', { sessionId: 'session-overview', scopeKey: 'global' });
|
|
828
|
+
assert.equal(ovGlobal.status, 200);
|
|
829
|
+
assert.equal(ovGlobal.body.selected.kind, 'global');
|
|
830
|
+
assert.ok(ovGlobal.body.selected.records.some((row) => row.title === '观察全局'));
|
|
831
|
+
|
|
832
|
+
// 详情按服务端枚举过的 scopeKey 取;内部路径、未知键一律不放行
|
|
833
|
+
const ovBody = await call('/lite-memory/body', { sessionId: 'session-overview', id: ovFirst.id, scopeKey: ov.body.current.key });
|
|
834
|
+
assert.equal(ovBody.status, 200);
|
|
835
|
+
assert.equal(ovBody.body.body, '开场三句内给结论。');
|
|
836
|
+
assert.equal(ovBody.body.scopeLabel, path.basename(ovCwd));
|
|
837
|
+
const ovOtherBody = await call('/lite-memory/body', { sessionId: 'session-overview', id: ovOther.id, scopeKey: store.scopeOf(ovOtherCwd).key });
|
|
838
|
+
assert.equal(ovOtherBody.status, 200);
|
|
839
|
+
assert.equal(ovOtherBody.body.scopeLabel, path.basename(ovOtherCwd));
|
|
840
|
+
assert.equal((await call('/lite-memory/body', { sessionId: 'session-overview', id: ovFirst.id, scopeKey: 'nope' })).status, 404);
|
|
841
|
+
assert.equal((await call('/lite-memory/body', { sessionId: 'session-overview', id: ovFirst.id, scopeKey: '../x' })).status, 404);
|
|
842
|
+
assert.equal((await call('/lite-memory/overview', {})).status, 400);
|
|
843
|
+
assert.equal((await call('/lite-memory/overview', { sessionId: 'session-overview' }, { origin: 'http://evil.example' })).status, 403);
|
|
844
|
+
const ovUnknownCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-overview-unknown-'));
|
|
845
|
+
sessionCwds.set('session-overview-unknown', ovUnknownCwd);
|
|
846
|
+
const ovUnconfirmedSaved = process.env.PATH;
|
|
847
|
+
process.env.PATH = '';
|
|
848
|
+
const ovUnconfirmed = await call('/lite-memory/overview', { sessionId: 'session-overview-unknown' });
|
|
849
|
+
process.env.PATH = ovUnconfirmedSaved;
|
|
850
|
+
assert.equal(ovUnconfirmed.status, 409);
|
|
851
|
+
assert.equal(fs.existsSync(path.join(store.scopeOf(ovUnknownCwd).dir, 'state.json')), false);
|
|
852
|
+
|
|
853
|
+
// 实验条目也画进图里,但只有面板能看见;普通读取面仍然拿不到
|
|
854
|
+
state.transact(store.scopeOf(ovCwd), {}, (draft) => {
|
|
855
|
+
const row = draft.entries.find((entry) => entry.id === ovSecond.id);
|
|
856
|
+
row.origin = 'project_evolution';
|
|
857
|
+
row.maintenance = { ...(row.maintenance || {}), learning: { state: 'trial', source: 'evolution' } };
|
|
858
|
+
return { changed: true };
|
|
859
|
+
});
|
|
860
|
+
const ovExperimental = await call('/lite-memory/overview', { sessionId: 'session-overview' });
|
|
861
|
+
assert.equal(ovExperimental.body.selected.stats.experimental, 1);
|
|
862
|
+
assert.equal(ovExperimental.body.selected.records.find((row) => row.id === ovSecond.id).experimental, true);
|
|
863
|
+
assert.equal(store.loadIndex(store.scopeOf(ovCwd), { allowExperimental: true }).entries.some((row) => row.id === ovSecond.id), true);
|
|
864
|
+
|
|
865
|
+
// 归档不是死数字:总数诚实,列表单独取,正文还能按 id 打开
|
|
866
|
+
state.transact(store.scopeOf(ovCwd), {}, (draft) => {
|
|
867
|
+
const row = draft.entries.find((entry) => entry.id === ovSecond.id);
|
|
868
|
+
row.status = 'archived';
|
|
869
|
+
return { changed: true };
|
|
870
|
+
});
|
|
871
|
+
const ovArchived = await call('/lite-memory/overview', { sessionId: 'session-overview' });
|
|
872
|
+
assert.equal(ovArchived.body.selected.stats.archived, 1);
|
|
873
|
+
assert.equal(ovArchived.body.selected.records.some((row) => row.id === ovSecond.id), false);
|
|
874
|
+
assert.equal(ovArchived.body.selected.archived.some((row) => row.id === ovSecond.id), true);
|
|
875
|
+
const ovArchivedBody = await call('/lite-memory/body', { sessionId: 'session-overview', id: ovSecond.id, scopeKey: ovArchived.body.selected.key });
|
|
876
|
+
assert.equal(ovArchivedBody.status, 200);
|
|
877
|
+
assert.equal(ovArchivedBody.body.status, 'archived');
|
|
878
|
+
assert.equal(ovArchivedBody.body.body, '右侧留白。');
|
|
879
|
+
|
|
677
880
|
console.log('contract.test.mjs: all passed');
|
|
@@ -0,0 +1,297 @@
|
|
|
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-control-'));
|
|
7
|
+
const { apply } = await import('../index.js');
|
|
8
|
+
const store = await import('../store.js');
|
|
9
|
+
const state = await import('../state.js');
|
|
10
|
+
|
|
11
|
+
const effect = (fn) => {
|
|
12
|
+
const dispose = fn();
|
|
13
|
+
return typeof dispose === 'function' ? dispose : () => {};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function boot() {
|
|
17
|
+
const defs = new Map();
|
|
18
|
+
const handlers = new Map();
|
|
19
|
+
const promptContexts = [];
|
|
20
|
+
const ctx = {
|
|
21
|
+
tools: { register: (def) => { defs.set(def.name, def); return () => {}; } },
|
|
22
|
+
effect,
|
|
23
|
+
inject: () => () => {},
|
|
24
|
+
get: (name) => (name === 'systemPrompt'
|
|
25
|
+
? { context: (entry) => { promptContexts.push(entry); return () => {}; } }
|
|
26
|
+
: name === 'agents' ? { currentInitiator: () => null } : undefined),
|
|
27
|
+
on: (event, handler) => {
|
|
28
|
+
const list = handlers.get(event) || [];
|
|
29
|
+
list.push(handler);
|
|
30
|
+
handlers.set(event, list);
|
|
31
|
+
return () => {};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
apply(ctx, { mode: 'adaptive' });
|
|
35
|
+
const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
|
|
36
|
+
const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
|
|
37
|
+
const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
|
|
38
|
+
return { exec, preStep, text: (agent, signal) => provider.text({ agent, signal }), debug: globalThis.__liteMemoryRecall };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let seq = 0;
|
|
42
|
+
const human = (body) => { seq += 1; return { role: 'user', id: `msg-${seq}`, source: { kind: 'user' }, content: [{ type: 'text', text: body }] }; };
|
|
43
|
+
const enter = (...messages) => ({ kind: 'enter', messages });
|
|
44
|
+
const mkdir = (label) => fs.mkdtempSync(path.join(os.tmpdir(), `lite-memory-control-${label}-`));
|
|
45
|
+
const makeAgent = (id, dir) => ({ id, cwd: dir, session: { id: `session-${id}`, header: { cwd: dir } } });
|
|
46
|
+
const save = (scope, fields) => store.upsert(scope, {
|
|
47
|
+
applicability: 'all_in_scope',
|
|
48
|
+
body: `正文:${fields.title}`,
|
|
49
|
+
resolution: 'distinct',
|
|
50
|
+
...fields,
|
|
51
|
+
summary: fields.summary ?? `${fields.title}的摘要`,
|
|
52
|
+
});
|
|
53
|
+
const seedRule = (scope, title) => {
|
|
54
|
+
const saved = save(scope, { title, tags: ['口播'] });
|
|
55
|
+
const id = saved.record?.id ?? saved.id;
|
|
56
|
+
state.transact(scope, {}, (draft) => {
|
|
57
|
+
const row = draft.entries.find((entry) => entry.id === id);
|
|
58
|
+
row.origin = 'project_evolution';
|
|
59
|
+
row.current.conditions = [{ key: 'seriesId', value: '口播' }];
|
|
60
|
+
row.maintenance.learning = { state: 'active' };
|
|
61
|
+
return { changed: true };
|
|
62
|
+
});
|
|
63
|
+
return id;
|
|
64
|
+
};
|
|
65
|
+
const scopeOf = (dir) => store.scopeOf(dir);
|
|
66
|
+
const toggle = (scope, enabled) => store.setMemoryEnabled(scope, enabled, { operationId: `op-${Math.random().toString(16).slice(2)}` });
|
|
67
|
+
|
|
68
|
+
/* C01:总开关关闭后,自动注入、全局召回、检索、读取、写入、学习全部停。 */
|
|
69
|
+
{
|
|
70
|
+
const dir = mkdir('off');
|
|
71
|
+
const scope = scopeOf(dir);
|
|
72
|
+
const globalSaved = save(store.globalScope(), { title: '全局写作偏好', tags: ['写作'] });
|
|
73
|
+
const globalId = globalSaved.record?.id ?? globalSaved.id;
|
|
74
|
+
save(scope, { title: '项目构建约定', tags: ['构建'] });
|
|
75
|
+
seedRule(scope, '口播开头');
|
|
76
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-on' }).ok, true);
|
|
77
|
+
assert.equal(toggle(scope, false).ok, true);
|
|
78
|
+
assert.equal(store.controlStatus(scope).enabled, false);
|
|
79
|
+
|
|
80
|
+
const runtime = boot();
|
|
81
|
+
const agent = makeAgent('a-off', dir);
|
|
82
|
+
const sig = new AbortController().signal;
|
|
83
|
+
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('按全局写作偏好来修构建脚本')));
|
|
84
|
+
assert.equal(runtime.text(agent, sig), '');
|
|
85
|
+
assert.equal(runtime.debug.preparedOf(agent), null);
|
|
86
|
+
|
|
87
|
+
const found = await runtime.exec('memory_search', { query: '写作 构建 口播开头' }, agent);
|
|
88
|
+
assert.equal(found.status, 'memory_disabled');
|
|
89
|
+
assert.equal(found.count, 0);
|
|
90
|
+
assert.ok(globalId);
|
|
91
|
+
assert.equal((await runtime.exec('memory_read', { id: globalId }, agent)).status, 'memory_disabled');
|
|
92
|
+
assert.equal((await runtime.exec('memory_forget', { id: globalId, mode: 'archive' }, agent)).mode, 'memory_disabled');
|
|
93
|
+
const suggested = await runtime.exec('memory_suggest', { title: '新偏好', summary: '一条偏好' }, agent);
|
|
94
|
+
assert.equal(suggested.asked, false);
|
|
95
|
+
const saved = await runtime.exec('memory_save', { title: '关闭后写入', summary: '应当拒绝', body: '不该落盘' }, agent);
|
|
96
|
+
assert.equal(saved.changed, false);
|
|
97
|
+
assert.equal(saved.reason, 'memory_disabled');
|
|
98
|
+
assert.equal(saved.id, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* C02:关闭期间所有写入入口都拒绝,自进化不能绕过总开关打开。 */
|
|
102
|
+
{
|
|
103
|
+
const dir = mkdir('write');
|
|
104
|
+
const scope = scopeOf(dir);
|
|
105
|
+
assert.equal(toggle(scope, false).ok, true);
|
|
106
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-blocked' }).ok, false);
|
|
107
|
+
const saved = store.saveEntry(scope, { title: '一条项目规范', summary: '普通摘要', body: '正文', kind: 'convention', scope: 'project', durability: 'durable' });
|
|
108
|
+
assert.equal(saved.reasonCode, 'memory_disabled');
|
|
109
|
+
const task = store.saveTaskState(scope, { version: 1, sessions: {}, episodes: {}, defaults: null, revision: 1 }, { operationId: 'task-blocked' });
|
|
110
|
+
assert.equal(task.ok, false);
|
|
111
|
+
const committed = store.commitLearning(scope, { title: '学习规则', summary: '摘要', body: '正文' }, {}, { operationId: 'learn-blocked' });
|
|
112
|
+
assert.equal(committed.ok, false);
|
|
113
|
+
const run = store.setEvolution(scope, false, { operationId: 'evo-off' });
|
|
114
|
+
assert.equal(run.ok, false);
|
|
115
|
+
assert.equal(run.code, 'memory_disabled');
|
|
116
|
+
assert.equal(store.controlStatus(scope).enabled, false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* C03:关一个项目不影响另一个;重新打开记忆后自进化仍保持关闭。 */
|
|
120
|
+
{
|
|
121
|
+
const dirA = mkdir('iso-a');
|
|
122
|
+
const dirB = mkdir('iso-b');
|
|
123
|
+
const scopeA = scopeOf(dirA);
|
|
124
|
+
const scopeB = scopeOf(dirB);
|
|
125
|
+
save(scopeB, { title: 'B 项目构建约定', tags: ['构建', '脚本'] });
|
|
126
|
+
seedRule(scopeB, '口播开头');
|
|
127
|
+
assert.equal(store.setEvolution(scopeB, true, { operationId: 'evo-b' }).ok, true);
|
|
128
|
+
assert.equal(toggle(scopeA, false).ok, true);
|
|
129
|
+
assert.equal(store.evolutionStatus(scopeB).enabled, true);
|
|
130
|
+
assert.equal(store.controlStatus(scopeB).enabled, true);
|
|
131
|
+
|
|
132
|
+
const runtime = boot();
|
|
133
|
+
const agentB = makeAgent('b-live', dirB);
|
|
134
|
+
const sigB = new AbortController().signal;
|
|
135
|
+
await runtime.preStep({ agent: agentB, signal: sigB, turn: 't1' }, enter(human('按项目构建约定改脚本')));
|
|
136
|
+
assert.match(runtime.text(agentB, sigB), /B 项目构建约定/);
|
|
137
|
+
|
|
138
|
+
toggle(scopeB, false);
|
|
139
|
+
assert.equal(store.evolutionStatus(scopeB).enabled, false);
|
|
140
|
+
assert.equal(store.evolutionStatus(scopeB).configuredEnabled, false);
|
|
141
|
+
toggle(scopeB, true);
|
|
142
|
+
assert.equal(store.evolutionStatus(scopeB).enabled, false);
|
|
143
|
+
assert.equal(store.setEvolution(scopeB, true, { operationId: 'evo-b-again' }).ok, true);
|
|
144
|
+
assert.equal(store.evolutionStatus(scopeB).enabled, true);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* C04:切换总开关让旧任务上下文立即失效,不拿关闭前的绑定继续注入。 */
|
|
148
|
+
{
|
|
149
|
+
const dir = mkdir('stale');
|
|
150
|
+
const scope = scopeOf(dir);
|
|
151
|
+
seedRule(scope, '口播开头');
|
|
152
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-stale' }).ok, true);
|
|
153
|
+
const runtime = boot();
|
|
154
|
+
const agent = makeAgent('a-stale', dir);
|
|
155
|
+
const sig = new AbortController().signal;
|
|
156
|
+
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
|
|
157
|
+
const held = runtime.debug.windowOf(agent).experiment;
|
|
158
|
+
assert.ok(held && held.contextId);
|
|
159
|
+
toggle(scope, false);
|
|
160
|
+
toggle(scope, true);
|
|
161
|
+
const stale = await runtime.exec('memory_search', { query: '口播开头', contextId: held.contextId }, agent);
|
|
162
|
+
assert.equal(stale.status !== 'ok', true);
|
|
163
|
+
assert.equal((await runtime.exec('memory_search', { query: '口播开头' }, agent)).status, 'ok');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* C05:关闭保留候选、试用、结果与整理回执;重开记忆后自进化仍是关闭。 */
|
|
167
|
+
{
|
|
168
|
+
const dir = mkdir('preserve');
|
|
169
|
+
const scope = scopeOf(dir);
|
|
170
|
+
const id = seedRule(scope, '口播开头');
|
|
171
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-preserve' }).ok, true);
|
|
172
|
+
const before = store.learningSnapshot(scope);
|
|
173
|
+
assert.equal(before.counts.active, 1);
|
|
174
|
+
assert.equal(toggle(scope, false).ok, true);
|
|
175
|
+
const closed = store.learningSnapshot(scope);
|
|
176
|
+
assert.equal(closed.enabled, false);
|
|
177
|
+
assert.equal(closed.rules.length, before.rules.length);
|
|
178
|
+
assert.equal(closed.counts.active, 1);
|
|
179
|
+
assert.equal(toggle(scope, true).ok, true);
|
|
180
|
+
const reopened = store.learningSnapshot(scope);
|
|
181
|
+
assert.equal(reopened.enabled, false);
|
|
182
|
+
assert.equal(reopened.counts.active, 1);
|
|
183
|
+
assert.equal(store.evolutionStatus(scope).configuredEnabled, false);
|
|
184
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-resume' }).ok, true);
|
|
185
|
+
assert.equal(store.learningSnapshot(scope).counts.active, 1);
|
|
186
|
+
assert.ok(id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* C06:关闭期间管理面写操作一律拒绝,只读快照不写盘。 */
|
|
190
|
+
{
|
|
191
|
+
const dir = mkdir('managed-off');
|
|
192
|
+
const scope = scopeOf(dir);
|
|
193
|
+
const id = seedRule(scope, '口播开头');
|
|
194
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-managed' }).ok, true);
|
|
195
|
+
toggle(scope, false);
|
|
196
|
+
const file = state.stateFiles(scope).current;
|
|
197
|
+
const before = fs.readFileSync(file);
|
|
198
|
+
const snapshot = store.learningSnapshot(scope);
|
|
199
|
+
const rule = snapshot.rules[0];
|
|
200
|
+
assert.equal(rule.id, id);
|
|
201
|
+
assert.equal(Buffer.compare(before, fs.readFileSync(file)), 0);
|
|
202
|
+
for (const action of ['pin', 'pause', 'resume', 'delete', 'undo', 'adopt', 'edit']) {
|
|
203
|
+
let result;
|
|
204
|
+
if (action === 'pin') result = store.pinLearningRule(scope, id, { pinned: true, expectedRevision: rule.revision });
|
|
205
|
+
if (action === 'pause') result = store.pauseLearningRules(scope, [id], { operationId: 'pause-off' });
|
|
206
|
+
if (action === 'resume') result = store.resumeLearningRule(scope, id);
|
|
207
|
+
if (action === 'delete') result = store.removeLearningRule(scope, id, { expectedRevision: rule.revision });
|
|
208
|
+
if (action === 'undo') result = store.undoLearningChange(scope, id, { expectedRevision: rule.revision });
|
|
209
|
+
if (action === 'adopt') result = store.adoptLearningRule(scope, id, { expectedRevision: rule.revision });
|
|
210
|
+
if (action === 'edit') result = store.editLearningRule(scope, id, { expectedRevision: rule.revision, body: '改过的正文' });
|
|
211
|
+
assert.equal(result.ok, false, action);
|
|
212
|
+
assert.equal(result.code, 'memory_disabled', action);
|
|
213
|
+
}
|
|
214
|
+
assert.equal(Buffer.compare(before, fs.readFileSync(file)), 0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* C07:采纳把候选转为生效;编辑只保存并留上一版,不改状态以外的东西。 */
|
|
218
|
+
{
|
|
219
|
+
const dir = mkdir('adopt');
|
|
220
|
+
const scope = scopeOf(dir);
|
|
221
|
+
const id = seedRule(scope, '口播开头');
|
|
222
|
+
const first = store.learningSnapshot(scope).rules[0];
|
|
223
|
+
const edited = store.editLearningRule(scope, id, { expectedRevision: first.revision, body: '开头两句内给结论。', title: '口播结论前置' });
|
|
224
|
+
assert.equal(edited.ok, true);
|
|
225
|
+
const afterEdit = store.learningSnapshot(scope).rules[0];
|
|
226
|
+
assert.equal(afterEdit.state, 'trial');
|
|
227
|
+
assert.equal(afterEdit.body, '开头两句内给结论。');
|
|
228
|
+
assert.equal(afterEdit.title, '口播结论前置');
|
|
229
|
+
assert.equal(afterEdit.learning.versions.length, 1);
|
|
230
|
+
const adopted = store.adoptLearningRule(scope, id, { expectedRevision: afterEdit.revision });
|
|
231
|
+
assert.equal(adopted.ok, true);
|
|
232
|
+
const afterAdopt = store.learningSnapshot(scope).rules[0];
|
|
233
|
+
assert.equal(afterAdopt.state, 'active');
|
|
234
|
+
assert.equal(afterAdopt.body, '开头两句内给结论。');
|
|
235
|
+
assert.equal(afterAdopt.title, '口播结论前置');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* C07b:只改标题不动正文时保留生效状态与学习依据。 */
|
|
239
|
+
{
|
|
240
|
+
const dir = mkdir('title-only');
|
|
241
|
+
const scope = scopeOf(dir);
|
|
242
|
+
const id = seedRule(scope, '口播开头');
|
|
243
|
+
state.transact(scope, {}, (draft) => {
|
|
244
|
+
const row = draft.entries.find((entry) => entry.id === id);
|
|
245
|
+
row.maintenance.learning = { state: 'active', evidence: [{ text: '三次口播都先改开头' }] };
|
|
246
|
+
return { changed: true };
|
|
247
|
+
});
|
|
248
|
+
const first = store.learningSnapshot(scope).rules[0];
|
|
249
|
+
const edited = store.editLearningRule(scope, id, { expectedRevision: first.revision, title: '口播开头重命名' });
|
|
250
|
+
assert.equal(edited.ok, true);
|
|
251
|
+
const after = store.learningSnapshot(scope).rules[0];
|
|
252
|
+
assert.equal(after.state, 'active');
|
|
253
|
+
assert.equal(after.learning.evidence.length, 1);
|
|
254
|
+
assert.equal(after.title, '口播开头重命名');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* C08:关闭保留已消耗额度与默认绑定,重开后仍按原额度限额。 */
|
|
258
|
+
{
|
|
259
|
+
const dir = mkdir('preserve-quota');
|
|
260
|
+
const scope = scopeOf(dir);
|
|
261
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-quota' }).ok, true);
|
|
262
|
+
const defaults = { taskKind: 'video', seriesId: '口播' };
|
|
263
|
+
assert.equal(store.saveTaskState(scope, { version: 1, sessions: {}, episodes: {}, defaults, revision: 1 }, { operationId: 'defaults' }).ok, true);
|
|
264
|
+
assert.equal(store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a1' }).ok, true);
|
|
265
|
+
assert.equal(store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a2' }).ok, true);
|
|
266
|
+
assert.equal(store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a3' }).ok, false);
|
|
267
|
+
toggle(scope, false);
|
|
268
|
+
toggle(scope, true);
|
|
269
|
+
assert.deepEqual(store.taskState(scope).defaults, { ...defaults, confirmedBy: 'user', confirmedAt: '' });
|
|
270
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-quota-again' }).ok, true);
|
|
271
|
+
const limited = store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a4' });
|
|
272
|
+
assert.equal(limited.ok, false);
|
|
273
|
+
assert.equal(limited.reason, 'episode_limit');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* C09:同一项目两个会话都已准备内容,关闭后两边都立即失效。 */
|
|
277
|
+
{
|
|
278
|
+
const dir = mkdir('two-sessions');
|
|
279
|
+
const scope = scopeOf(dir);
|
|
280
|
+
seedRule(scope, '口播开头');
|
|
281
|
+
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-two' }).ok, true);
|
|
282
|
+
const runtime = boot();
|
|
283
|
+
const a1 = makeAgent('two-1', dir);
|
|
284
|
+
const a2 = makeAgent('two-2', dir);
|
|
285
|
+
const sig = new AbortController().signal;
|
|
286
|
+
await runtime.preStep({ agent: a1, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
|
|
287
|
+
await runtime.preStep({ agent: a2, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
|
|
288
|
+
assert.match(runtime.text(a1, sig), /口播开头/);
|
|
289
|
+
assert.match(runtime.text(a2, sig), /口播开头/);
|
|
290
|
+
toggle(scope, false);
|
|
291
|
+
assert.equal(runtime.text(a1, sig), '');
|
|
292
|
+
assert.equal(runtime.text(a2, sig), '');
|
|
293
|
+
assert.equal(runtime.debug.preparedOf(a1).text, '');
|
|
294
|
+
assert.equal(runtime.debug.preparedOf(a2).text, '');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log('control.test.mjs: all passed');
|