dsh-memu-memory 1.2.1 → 1.2.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/AGENTS.md +1 -1
- package/README.md +12 -5
- package/README.zh.md +4 -2
- package/docs/contributing.md +16 -3
- package/docs/host-api.md +3 -1
- package/docs/memory-semantics.md +21 -2
- package/docs/troubleshooting.md +42 -4
- package/lib/backup.js +24 -2
- package/lib/binding-diagnostics.js +2 -2
- package/lib/client.js +1361 -146
- package/lib/consolidation-worker.js +30 -8
- package/lib/curator-evidence.js +131 -0
- package/lib/curator-policy.js +81 -0
- package/lib/curator.js +1252 -91
- package/lib/cycle.js +3 -0
- package/lib/embedding-download.js +39 -4
- package/lib/engine.js +6 -3
- package/lib/global-graph.js +282 -0
- package/lib/graph.js +51 -9
- package/lib/inbound.js +19 -0
- package/lib/inject.js +277 -17
- package/lib/lexicon.js +1115 -0
- package/lib/local-vector-service.js +8 -6
- package/lib/managed-runtime.js +33 -2
- package/lib/memory.js +282 -17
- package/lib/memu.js +2 -2
- package/lib/migrate.js +83 -2
- package/lib/private-links.js +587 -0
- package/lib/private.js +32 -3
- package/lib/prompts/global-curator.md +76 -19
- package/lib/prompts/private-consolidation.coding.md +4 -1
- package/lib/prompts/private-consolidation.md +3 -1
- package/lib/prompts/private-consolidation.mixed.md +3 -1
- package/lib/prompts/private-consolidation.rp.md +3 -1
- package/lib/prompts.js +1 -1
- package/lib/rpc.js +577 -9
- package/lib/state.js +4 -1
- package/lib/text-retrieval.js +661 -0
- package/lib/text-similarity.js +36 -18
- package/lib/tools.js +168 -10
- package/lib/webui/standalone.css +17 -0
- package/lib/webui/standalone.js +2 -0
- package/package.json +1 -1
- package/scripts/acceptance-local.mjs +8 -5
- package/scripts/curator-pair-calibrate.mjs +259 -0
- package/scripts/lexicon-calibrate.mjs +230 -0
- package/scripts/lexicon-dedupe.mjs +318 -0
- package/tests/absorb-review.test.mjs +353 -0
- package/tests/appendix-b2.test.mjs +2 -2
- package/tests/client-export.test.mjs +520 -0
- package/tests/client-render.test.mjs +19 -3
- package/tests/consolidation-fork.test.mjs +14 -12
- package/tests/consolidation-snapshot.test.mjs +39 -0
- package/tests/curator-absorb.test.mjs +443 -0
- package/tests/curator-calibrate.test.mjs +173 -0
- package/tests/curator-clusters.test.mjs +281 -0
- package/tests/curator-commit.test.mjs +52 -5
- package/tests/curator-evidence.test.mjs +102 -0
- package/tests/curator-link.test.mjs +316 -0
- package/tests/curator-pairing.test.mjs +252 -0
- package/tests/curator-probe.test.mjs +15 -6
- package/tests/curator-review-mode.test.mjs +314 -0
- package/tests/disclosure.test.mjs +2 -1
- package/tests/fixtures/curator-pairs/labeled.v1.jsonl +90 -0
- package/tests/fixtures/lexicon-stub-hooks.mjs +15 -0
- package/tests/fixtures/lexicon-stub.mjs +284 -0
- package/tests/graph-cascade.test.mjs +35 -2
- package/tests/graph.test.mjs +96 -0
- package/tests/help-page.test.mjs +415 -0
- package/tests/inbound.test.mjs +6 -3
- package/tests/inject-guide.test.mjs +28 -0
- package/tests/inject.test.mjs +157 -5
- package/tests/lexicon-calibrate.test.mjs +48 -0
- package/tests/lexicon-dedupe.test.mjs +171 -0
- package/tests/lexicon-frequency.test.mjs +179 -0
- package/tests/lexicon-inject.test.mjs +410 -0
- package/tests/lexicon-perf.test.mjs +436 -0
- package/tests/lexicon-ui.test.mjs +547 -0
- package/tests/lexicon.test.mjs +922 -0
- package/tests/managed-runtime.test.mjs +32 -0
- package/tests/private-links.test.mjs +445 -0
- package/tests/prompts.test.mjs +35 -0
- package/tests/recall-dedup.test.mjs +33 -4
- package/tests/tool-schema.test.mjs +1 -1
- package/tests/tools.test.mjs +112 -3
- package/tests/webui-auth.test.mjs +348 -2
- package/tests/webui.test.mjs +188 -0
package/tests/webui.test.mjs
CHANGED
|
@@ -8,6 +8,7 @@ process.env.DSH_MEMU_HOME = home
|
|
|
8
8
|
const state = await import('../lib/state.js')
|
|
9
9
|
const { registerRpc, dispatch } = await import('../lib/rpc.js')
|
|
10
10
|
const { webUiHtml, standaloneHtml, standaloneAsset } = await import('../lib/webui.js')
|
|
11
|
+
const { dataDir } = await import('../lib/paths.js')
|
|
11
12
|
test.after(() => { state.flushState(); fs.rmSync(home, { recursive: true, force: true }) })
|
|
12
13
|
function handler(rejection) {
|
|
13
14
|
const routes = []
|
|
@@ -133,6 +134,47 @@ test('wall heartbeat retries failed paints, pauses hidden and shares the metadat
|
|
|
133
134
|
assert.match(source, /result !== false/)
|
|
134
135
|
assert.match(source, /data\.type === "memory-write"/)
|
|
135
136
|
})
|
|
137
|
+
|
|
138
|
+
test('single-library browseWall still carries global rows and F1/F2 metadata', async () => {
|
|
139
|
+
const library = state.createLibrary({ name: 'wall-global-merge', cwd: path.join(home, 'wall-global-merge') })
|
|
140
|
+
state.updatePrivateIndex(library.id, (index) => {
|
|
141
|
+
index.entries['mem-private-merge'] = {
|
|
142
|
+
title: 'private row', content: 'private row body', status: 'active', layer: 'long_term',
|
|
143
|
+
visibility: 'shared', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
state.saveGlobalIndex({
|
|
147
|
+
version: 1,
|
|
148
|
+
entries: {
|
|
149
|
+
'gmem-linked': {
|
|
150
|
+
title: 'global row', content: 'global row body', status: 'active', domain: 'general', track: 'memory',
|
|
151
|
+
links: [{ id: 'lnk-probe', opId: 'op-probe', to: 'gmem-other', kind: 'related', note: '这两条讲同一件事,必须一起看。', confidence: 0.7, evidence: [], createdAt: new Date().toISOString() }],
|
|
152
|
+
absorbedFrom: ['gmem-a', 'gmem-b'],
|
|
153
|
+
updatedAt: new Date().toISOString(),
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
state.flushState()
|
|
158
|
+
|
|
159
|
+
const wall = await dispatch({}, 'browseWall', { libraryId: library.id })
|
|
160
|
+
assert.ok(wall.items.some((item) => item.id === 'mem-private-merge'), 'the authorized private row is present')
|
|
161
|
+
const globalRow = wall.items.find((item) => item.id === 'gmem-linked')
|
|
162
|
+
assert.ok(globalRow, 'a single-library wall must keep the shared global scope')
|
|
163
|
+
assert.equal(globalRow.libraryId, null)
|
|
164
|
+
assert.equal(globalRow.links.length, 1)
|
|
165
|
+
assert.deepEqual(globalRow.absorbedFrom, ['gmem-a', 'gmem-b'])
|
|
166
|
+
|
|
167
|
+
const listed = await dispatch({}, 'listGlobal', {})
|
|
168
|
+
const globalFile = listed.files.find((file) => file.id === 'gmem-linked')
|
|
169
|
+
assert.ok(globalFile, 'the global page lists the same row')
|
|
170
|
+
assert.equal(globalFile.links.length, 1)
|
|
171
|
+
assert.deepEqual(globalFile.absorbedFrom, ['gmem-a', 'gmem-b'])
|
|
172
|
+
|
|
173
|
+
const source = fs.readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
174
|
+
assert.match(source, /wallAllBlocked/, 'the single-library fallback explains itself')
|
|
175
|
+
assert.match(source, /browseWall', \{ libraryId: firstLibraryId/, 'the wall retries the first library after an all-libraries 403')
|
|
176
|
+
assert.match(source, /setMultiBlocked\(true\)/, 'the fallback disables the all-libraries option')
|
|
177
|
+
})
|
|
136
178
|
test('the settings button preserves the host SPA and links to individual pages', () => {
|
|
137
179
|
const client = fs.readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
138
180
|
// libraryId 选项:整理失败提醒的跳转按钮按库深链(?libraryId=…#/settings/browser)。
|
|
@@ -157,6 +199,152 @@ test('RPC still uses stable envelopes and rejects non-UI GETs', async () => {
|
|
|
157
199
|
assert.equal((await request('/dsh-memu-memory/listLibraries')).status, 405)
|
|
158
200
|
})
|
|
159
201
|
|
|
202
|
+
// ── 词典 RPC(§2.3 / §2.5):公开两态 active/archived,没有 pending/review ──
|
|
203
|
+
function lexiconFile(libraryId) {
|
|
204
|
+
return path.join(dataDir(), 'lexicon', libraryId + '.json')
|
|
205
|
+
}
|
|
206
|
+
function writeLexiconEntries(libraryId, entries) {
|
|
207
|
+
const file = lexiconFile(libraryId)
|
|
208
|
+
const store = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
209
|
+
Object.assign(store.terms, entries)
|
|
210
|
+
// 直接写盘模拟 legacy/手工数据;条目数变化保证 mtime+size 缓存必然失效。
|
|
211
|
+
fs.writeFileSync(file, JSON.stringify(store))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
test('lexicon RPC: archive/restore lifecycle, active/archived tabs, stable statusCounts and archived-aware search', async () => {
|
|
215
|
+
const library = state.createLibrary({ name: 'lexicon-rpc', cwd: path.join(home, 'lexicon-rpc') })
|
|
216
|
+
const first = await dispatch({}, 'lexiconAdd', { libraryId: library.id, term: '糖刀', kind: 'meme', meaning: '表面发糖、实际虐心的剧情' })
|
|
217
|
+
assert.equal(first.term.status, 'active', 'the management panel writes as the user → active, never pending')
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 6))
|
|
219
|
+
const second = await dispatch({}, 'lexiconAdd', { libraryId: library.id, term: '红牌', kind: 'trigger', sensitivity: 'sensitive', meaning: '角色行为的红线信号' })
|
|
220
|
+
const knifeId = first.term.termId
|
|
221
|
+
const redId = second.term.termId
|
|
222
|
+
|
|
223
|
+
const activeView = await dispatch({}, 'lexiconList', { libraryId: library.id })
|
|
224
|
+
assert.equal(activeView.total, 2)
|
|
225
|
+
assert.deepEqual(activeView.items.map((item) => item.id), [redId, knifeId], 'updatedAt desc: the newest term first')
|
|
226
|
+
assert.ok(activeView.items.every((item) => item.status === 'active'), 'the default tab is active only')
|
|
227
|
+
assert.deepEqual(activeView.statusCounts, { active: 2, archived: 0 })
|
|
228
|
+
assert.deepEqual(activeView.counts, { slang: 0, meme: 1, trigger: 1, term: 0 })
|
|
229
|
+
|
|
230
|
+
const archived = await dispatch({}, 'lexiconArchive', { libraryId: library.id, termId: knifeId })
|
|
231
|
+
assert.equal(archived.libraryId, library.id)
|
|
232
|
+
assert.equal(archived.termId, knifeId)
|
|
233
|
+
assert.equal(archived.action, 'archive')
|
|
234
|
+
assert.equal(archived.term.status, 'archived')
|
|
235
|
+
|
|
236
|
+
const afterArchive = await dispatch({}, 'lexiconList', { libraryId: library.id })
|
|
237
|
+
assert.deepEqual(afterArchive.items.map((item) => item.id), [redId], 'archived rows leave the active tab')
|
|
238
|
+
assert.deepEqual(afterArchive.statusCounts, { active: 1, archived: 1 }, 'tab counts are computed before status/kind/search filters')
|
|
239
|
+
assert.deepEqual(afterArchive.counts, { slang: 0, meme: 0, trigger: 1, term: 0 }, 'kind counts follow the rows currently selected')
|
|
240
|
+
|
|
241
|
+
const archivedTab = await dispatch({}, 'lexiconList', { libraryId: library.id, status: 'archived' })
|
|
242
|
+
assert.deepEqual(archivedTab.items.map((item) => item.id), [knifeId])
|
|
243
|
+
assert.deepEqual(archivedTab.counts, { slang: 0, meme: 1, trigger: 0, term: 0 })
|
|
244
|
+
assert.deepEqual(archivedTab.statusCounts, { active: 1, archived: 1 })
|
|
245
|
+
|
|
246
|
+
const both = await dispatch({}, 'lexiconList', { libraryId: library.id, includeArchived: true })
|
|
247
|
+
assert.deepEqual(both.items.map((item) => item.id).sort(), [knifeId, redId].sort())
|
|
248
|
+
assert.equal(both.total, 2)
|
|
249
|
+
assert.deepEqual(both.statusCounts, { active: 1, archived: 1 })
|
|
250
|
+
|
|
251
|
+
const explicitWins = await dispatch({}, 'lexiconList', { libraryId: library.id, includeArchived: true, status: 'active' })
|
|
252
|
+
assert.deepEqual(explicitWins.items.map((item) => item.id), [redId], 'an explicit status outranks includeArchived')
|
|
253
|
+
|
|
254
|
+
// search 默认只搜 active;未激活的 includeArchived 才包含归档;旧的 includePending 没有任何语义。
|
|
255
|
+
assert.equal((await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '糖刀' })).items.length, 0)
|
|
256
|
+
const archivedSearch = await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '糖刀', includeArchived: true })
|
|
257
|
+
assert.equal(archivedSearch.items.length, 1)
|
|
258
|
+
assert.equal(archivedSearch.items[0].status, 'archived')
|
|
259
|
+
assert.equal((await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '糖刀', includePending: true })).items.length, 0, 'includePending is gone')
|
|
260
|
+
assert.equal((await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '红牌' })).items.length, 1)
|
|
261
|
+
|
|
262
|
+
const restored = await dispatch({}, 'lexiconRestore', { libraryId: library.id, termId: knifeId })
|
|
263
|
+
assert.equal(restored.action, 'restore')
|
|
264
|
+
assert.equal(restored.termId, knifeId)
|
|
265
|
+
assert.equal(restored.term.status, 'active')
|
|
266
|
+
assert.equal((await dispatch({}, 'lexiconList', { libraryId: library.id })).total, 2)
|
|
267
|
+
|
|
268
|
+
await assert.rejects(() => dispatch({}, 'lexiconRestore', { libraryId: library.id, termId: knifeId }), /only archived terms can be restored/)
|
|
269
|
+
await assert.rejects(() => dispatch({}, 'lexiconArchive', { libraryId: library.id, termId: 'lex-nope' }), /does not exist/)
|
|
270
|
+
await assert.rejects(() => dispatch({}, 'lexiconArchive', { libraryId: library.id }), /termId is required/)
|
|
271
|
+
await assert.rejects(() => dispatch({}, 'lexiconRestore', { libraryId: library.id }), /termId is required/)
|
|
272
|
+
// status 只认公开两态:legacy pending / rejected 明确报错,不静默当成“全都要”。
|
|
273
|
+
for (const status of ['pending', 'rejected', 'deleted']) {
|
|
274
|
+
await assert.rejects(() => dispatch({}, 'lexiconList', { libraryId: library.id, status }), /unknown lexicon status/)
|
|
275
|
+
}
|
|
276
|
+
// 旧方法彻底消失:lexiconReview 不再是 dispatch 的入口。
|
|
277
|
+
await assert.rejects(() => dispatch({}, 'lexiconReview', { libraryId: library.id, termId: knifeId, action: 'approve' }), /unknown endpoint/)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('lexiconList never returns pending/rejected tombstones; ordering ties break by id', async () => {
|
|
281
|
+
const library = state.createLibrary({ name: 'lexicon-tombstones', cwd: path.join(home, 'lexicon-tombstones') })
|
|
282
|
+
await dispatch({}, 'lexiconAdd', { libraryId: library.id, term: '归档条', kind: 'term', meaning: '会被归档的词' })
|
|
283
|
+
const archivedView = await dispatch({}, 'lexiconList', { libraryId: library.id })
|
|
284
|
+
const archivedId = archivedView.items.find((item) => item.term === '归档条').id
|
|
285
|
+
await dispatch({}, 'lexiconArchive', { libraryId: library.id, termId: archivedId })
|
|
286
|
+
const stamp = new Date().toISOString()
|
|
287
|
+
const base = { kind: 'term', meaning: 'legacy/手工数据', usage: '', aliases: [], normalizedAliases: [], examples: [], context: {}, visibility: 'shared', sensitivity: 'normal', source: 'model-proposal', createdAt: stamp, updatedAt: stamp }
|
|
288
|
+
// 同一时间戳的两条 active + 一条 rejected 墓碑 + 一条 legacy pending:
|
|
289
|
+
// 列表只出 active/archived,排序在时间戳相同处按 id 稳定兜底。
|
|
290
|
+
writeLexiconEntries(library.id, {
|
|
291
|
+
'lex-bbb': { ...base, term: '乙条', normalizedTerm: '乙条', status: 'active' },
|
|
292
|
+
'lex-aaa': { ...base, term: '甲条', normalizedTerm: '甲条', status: 'active' },
|
|
293
|
+
'lex-tombstone': { ...base, term: '墓碑词', normalizedTerm: '墓碑词', status: 'rejected', fingerprint: 'deadbeef' },
|
|
294
|
+
'lex-legacy-pending': { ...base, term: '老待审', normalizedTerm: '老待审', status: 'pending' },
|
|
295
|
+
})
|
|
296
|
+
const listed = await dispatch({}, 'lexiconList', { libraryId: library.id, includeArchived: true })
|
|
297
|
+
assert.deepEqual(listed.items.map((item) => item.term), ['甲条', '乙条', '归档条'], 'ties break by id; only active/archived rows are listed')
|
|
298
|
+
assert.deepEqual(listed.statusCounts, { active: 2, archived: 1 }, 'tombstones never count toward the tabs')
|
|
299
|
+
assert.ok(listed.items.every((item) => item.status === 'active' || item.status === 'archived'))
|
|
300
|
+
const activeOnly = await dispatch({}, 'lexiconList', { libraryId: library.id })
|
|
301
|
+
assert.deepEqual(activeOnly.items.map((item) => item.term), ['甲条', '乙条'])
|
|
302
|
+
assert.deepEqual(activeOnly.statusCounts, { active: 2, archived: 1 }, 'tab counts stay stable when the active tab is selected')
|
|
303
|
+
assert.equal((await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '墓碑词', includeArchived: true })).items.length, 0)
|
|
304
|
+
assert.equal((await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '老待审', includeArchived: true })).items.length, 0)
|
|
305
|
+
assert.equal((await dispatch({}, 'lexiconSearch', { libraryId: library.id, q: '甲条' })).items.length, 1)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('importMemories lexicon merge: rejected tombstones are never resurrected, legacy pending follows the migration mapping', async () => {
|
|
309
|
+
const library = state.createLibrary({ name: 'lexicon-import', cwd: path.join(home, 'lexicon-import') })
|
|
310
|
+
await dispatch({}, 'lexiconAdd', { libraryId: library.id, term: '目标条', kind: 'term', meaning: '现有 active 释义' })
|
|
311
|
+
const stamp = new Date().toISOString()
|
|
312
|
+
const base = { kind: 'term', meaning: 'x', usage: '', aliases: [], normalizedAliases: [], examples: [], context: {}, visibility: 'shared', sensitivity: 'normal', source: 'model-proposal', createdAt: stamp, updatedAt: stamp }
|
|
313
|
+
writeLexiconEntries(library.id, {
|
|
314
|
+
'lex-tombstone': { ...base, term: '墓碑词', normalizedTerm: '墓碑词', status: 'rejected', fingerprint: 'deadbeef' },
|
|
315
|
+
})
|
|
316
|
+
const memories = [{ id: 'mem-import-anchor', title: 'anchor', content: '让记忆导入有内容可写', status: 'active' }]
|
|
317
|
+
const report = await dispatch({}, 'importMemories', {
|
|
318
|
+
libraryId: library.id,
|
|
319
|
+
memories,
|
|
320
|
+
lexicon: { version: 1, terms: [
|
|
321
|
+
{ term: '墓碑词', kind: 'term', meaning: '背包里的墓碑,不许复活', status: 'rejected' },
|
|
322
|
+
{ term: '墓碑词', kind: 'term', meaning: '撞上库里的墓碑也不许复活', status: 'active' },
|
|
323
|
+
{ term: '目标条', kind: 'term', meaning: 'legacy 草稿试图覆盖正条', status: 'pending', revisionOf: 'lex-anything' },
|
|
324
|
+
{ term: '老提案', kind: 'term', meaning: 'legacy pending 提案', status: 'pending' },
|
|
325
|
+
{ term: '老草稿', kind: 'term', meaning: 'legacy revision 草稿', status: 'pending', revisionOf: 'lex-missing' },
|
|
326
|
+
{ term: '老敏感红牌', kind: 'trigger', meaning: 'legacy pending 敏感词', status: 'pending' },
|
|
327
|
+
] },
|
|
328
|
+
})
|
|
329
|
+
assert.equal(report.lexicon.skipped.length, 3, 'two rejected rows and the legacy draft over an active target are skipped')
|
|
330
|
+
assert.ok(report.lexicon.skipped.every((entry) => /tombstone|rejected|legacy pending draft/.test(entry.reason)))
|
|
331
|
+
assert.equal(report.lexicon.added, 3)
|
|
332
|
+
assert.equal(report.lexicon.failed.length, 0)
|
|
333
|
+
|
|
334
|
+
const listed = await dispatch({}, 'lexiconList', { libraryId: library.id, includeArchived: true })
|
|
335
|
+
const byTerm = new Map(listed.items.map((item) => [item.term, item]))
|
|
336
|
+
assert.equal(byTerm.has('墓碑词'), false, 'a rejected tombstone stays hidden and stays rejected')
|
|
337
|
+
assert.equal(byTerm.get('目标条').status, 'active', 'the active target keeps its own text')
|
|
338
|
+
assert.equal(byTerm.get('目标条').meaning, '现有 active 释义')
|
|
339
|
+
assert.equal(byTerm.get('老提案').status, 'active', 'legacy new proposal → active')
|
|
340
|
+
assert.equal(byTerm.get('老草稿').status, 'archived', 'legacy revision draft → archived')
|
|
341
|
+
assert.equal(byTerm.get('老敏感红牌').status, 'archived', 'legacy sensitive proposal → archived')
|
|
342
|
+
assert.deepEqual(listed.statusCounts, { active: 2, archived: 2 })
|
|
343
|
+
const raw = JSON.parse(fs.readFileSync(lexiconFile(library.id), 'utf8'))
|
|
344
|
+
assert.equal(raw.terms['lex-tombstone'].status, 'rejected', 'the tombstone row is untouched')
|
|
345
|
+
assert.equal(Object.values(raw.terms).some((entry) => entry.term === '墓碑词' && entry.status === 'active'), false)
|
|
346
|
+
})
|
|
347
|
+
|
|
160
348
|
test('old bookmarks and RPC URLs stay compatible after the package rename', async () => {
|
|
161
349
|
assert.equal((await request('/dsh-memu/ui')).body, standaloneHtml())
|
|
162
350
|
const res = await request('/dsh-memu/listLibraries', 'POST', { type: 'client-request', rpcId: 'legacy', method: 'listLibraries', payload: {} })
|