@ilaomianyang/dsh-memory-graph 0.10.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/CHANGELOG.md +259 -0
- package/LICENSE +21 -0
- package/README.en.md +466 -0
- package/README.md +459 -0
- package/client.js +1953 -0
- package/cordis.patch.yml +13 -0
- package/docs/auto-settings.png +0 -0
- package/docs/distill-button.png +0 -0
- package/docs/graph-dark.png +0 -0
- package/docs/graph-light.png +0 -0
- package/docs/session-tab-cards.png +0 -0
- package/docs/session-tab-open.png +0 -0
- package/docs/session-tab.png +0 -0
- package/docs/vault-panel.png +0 -0
- package/eval/REPORT.md +437 -0
- package/eval/questions.json +277 -0
- package/eval/run.mjs +460 -0
- package/eval/seed.mjs +489 -0
- package/icon.svg +26 -0
- package/index.js +243 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +77 -0
- package/src/auto.js +194 -0
- package/src/capture.js +237 -0
- package/src/config.js +280 -0
- package/src/distillapi.js +30 -0
- package/src/distillrun.js +106 -0
- package/src/extract.js +660 -0
- package/src/graph.js +1582 -0
- package/src/graphdata.js +475 -0
- package/src/http.js +32 -0
- package/src/pageapi.js +40 -0
- package/src/privacy.js +78 -0
- package/src/schema.js +323 -0
- package/src/search.js +317 -0
- package/src/tools.js +534 -0
- package/src/vault.js +888 -0
- package/test/auto.mjs +295 -0
- package/test/capture.mjs +264 -0
- package/test/client.mjs +1035 -0
- package/test/distillapi.mjs +212 -0
- package/test/extract.mjs +660 -0
- package/test/graphdata.mjs +295 -0
- package/test/live-supersede.mjs +78 -0
- package/test/live.mjs +156 -0
- package/test/page.mjs +116 -0
- package/test/schema.mjs +256 -0
- package/test/smoke.mjs +428 -0
- package/test/tools.mjs +238 -0
package/test/tools.mjs
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-surface test: every tool's execute + render path, with a stubbed ctx.
|
|
3
|
+
* node test/tools.mjs
|
|
4
|
+
*/
|
|
5
|
+
import assert from 'node:assert/strict'
|
|
6
|
+
import fsp from 'node:fs/promises'
|
|
7
|
+
import os from 'node:os'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
import { resolveConfig } from '../src/config.js'
|
|
11
|
+
import { createRedactor } from '../src/privacy.js'
|
|
12
|
+
import { createVault } from '../src/vault.js'
|
|
13
|
+
import { registerTools } from '../src/tools.js'
|
|
14
|
+
|
|
15
|
+
const dir = await fsp.mkdtemp(path.join(os.tmpdir(), 'memgraph-tools-'))
|
|
16
|
+
const cfg = resolveConfig({ vaultDir: dir })
|
|
17
|
+
const redactor = createRedactor(cfg.privacy)
|
|
18
|
+
const vault = createVault(cfg, redactor)
|
|
19
|
+
|
|
20
|
+
const registered = []
|
|
21
|
+
const ctx = {
|
|
22
|
+
tools: {
|
|
23
|
+
register(definition) {
|
|
24
|
+
registered.push(definition)
|
|
25
|
+
return () => {}
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
get() {
|
|
29
|
+
return undefined
|
|
30
|
+
},
|
|
31
|
+
logger: { info() {}, warn() {} },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
registerTools(ctx, { cfg, vault, redactor })
|
|
35
|
+
const byName = new Map(registered.map((definition) => [definition.name, definition]))
|
|
36
|
+
|
|
37
|
+
const results = []
|
|
38
|
+
function check(name, fn) {
|
|
39
|
+
try {
|
|
40
|
+
fn()
|
|
41
|
+
results.push(` ok ${name}`)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
results.push(` FAIL ${name}: ${error.message}`)
|
|
44
|
+
process.exitCode = 1
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function call(name, args) {
|
|
49
|
+
const definition = byName.get(name)
|
|
50
|
+
assert.ok(definition, `tool ${name} is not registered`)
|
|
51
|
+
const value = await definition.execute(args, { agent: null, signal: undefined })
|
|
52
|
+
const blocks = definition.output.render(args, value)
|
|
53
|
+
assert.ok(Array.isArray(blocks) && blocks.length > 0, `${name} render returned nothing`)
|
|
54
|
+
assert.equal(blocks[0].type, 'text', `${name} render did not produce text`)
|
|
55
|
+
assert.equal(typeof blocks[0].text, 'string')
|
|
56
|
+
return { value, text: blocks[0].text, definition }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ------------------------------------------------------------------ shape */
|
|
60
|
+
check('registers the eight memory tools', () => {
|
|
61
|
+
assert.deepEqual([...byName.keys()].sort(), [
|
|
62
|
+
'memory_distill',
|
|
63
|
+
'memory_forget',
|
|
64
|
+
'memory_get',
|
|
65
|
+
'memory_graph',
|
|
66
|
+
'memory_remember',
|
|
67
|
+
'memory_search',
|
|
68
|
+
'memory_stats',
|
|
69
|
+
'memory_timeline',
|
|
70
|
+
])
|
|
71
|
+
})
|
|
72
|
+
check('every tool has a name, description and object schema', () => {
|
|
73
|
+
for (const definition of registered) {
|
|
74
|
+
assert.ok(definition.name.startsWith('memory_'), definition.name)
|
|
75
|
+
assert.ok(definition.description.length > 40, `${definition.name} description too short`)
|
|
76
|
+
assert.equal(definition.parameters.type, 'object', definition.name)
|
|
77
|
+
assert.equal(typeof definition.output.render, 'function', definition.name)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
check('advertised kinds match what the vault accepts', () => {
|
|
81
|
+
const kinds = byName.get('memory_remember').parameters.properties.kind.enum
|
|
82
|
+
assert.deepEqual(kinds, ['decision', 'fact', 'open_question', 'preference', 'note'])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/* ------------------------------------------------------------------ remember */
|
|
86
|
+
const remembered = await call('memory_remember', {
|
|
87
|
+
kind: 'decision',
|
|
88
|
+
title: '插件通过 bundle patch 注入而不是改 profile 的 cordis.yml',
|
|
89
|
+
body: 'bundle 自带 cordis.patch.yml,安装后由 profile 的 package.json 引用,升级时不会覆盖用户的覆盖项。',
|
|
90
|
+
entities: ['dsh插件', 'bundle机制'],
|
|
91
|
+
confidence: 'user_stated',
|
|
92
|
+
})
|
|
93
|
+
check('remember writes a card and reports its id', () => {
|
|
94
|
+
assert.equal(remembered.value.ok, true)
|
|
95
|
+
assert.match(remembered.value.id, /^c-\d{8}-[0-9a-f]{4}$/)
|
|
96
|
+
assert.ok(remembered.text.includes(remembered.value.id), remembered.text)
|
|
97
|
+
})
|
|
98
|
+
check('default project falls back when there is no session', () => {
|
|
99
|
+
assert.equal(remembered.value.project, cfg.defaultProject)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const secret = await call('memory_remember', {
|
|
103
|
+
kind: 'fact',
|
|
104
|
+
title: '不要把这个 token 写进记忆',
|
|
105
|
+
body: '测试用的 key 是 sk-abcdefghijklmnopqrstuvwxyz0123456789,应该被替换掉。',
|
|
106
|
+
entities: ['隐私边界'],
|
|
107
|
+
})
|
|
108
|
+
const secretCard = await vault.getCard(secret.value.id)
|
|
109
|
+
check('a secret in the body is replaced before it hits the file', () => {
|
|
110
|
+
assert.equal(secretCard.body.includes('sk-abcdefghij'), false, secretCard.body)
|
|
111
|
+
assert.ok(secretCard.body.includes('[REDACTED]'), secretCard.body)
|
|
112
|
+
assert.ok(secret.text.includes('redacted'), secret.text)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/* ------------------------------------------------------------------ search */
|
|
116
|
+
const found = await call('memory_search', { query: '插件是怎么装上的' })
|
|
117
|
+
check('search finds what was just remembered', () => {
|
|
118
|
+
assert.ok(found.value.count >= 1, found.text)
|
|
119
|
+
assert.ok(found.text.includes(remembered.value.id), found.text)
|
|
120
|
+
})
|
|
121
|
+
const miss = await call('memory_search', { query: 'zzz 完全无关的东西' })
|
|
122
|
+
check('a miss is a plain answer, not an error', () => {
|
|
123
|
+
assert.equal(miss.value.count >= 0, true)
|
|
124
|
+
assert.ok(typeof miss.text === 'string')
|
|
125
|
+
})
|
|
126
|
+
const scoped = await call('memory_search', { query: '插件', project: 'no-such-project' })
|
|
127
|
+
check('project filter excludes other projects', () => {
|
|
128
|
+
assert.equal(scoped.value.count, 0, scoped.text)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
/* ------------------------------------------------------------------ get */
|
|
132
|
+
const got = await call('memory_get', { id: remembered.value.id })
|
|
133
|
+
check('get returns the full card and its entities', () => {
|
|
134
|
+
assert.equal(got.value.found, true)
|
|
135
|
+
assert.ok(got.text.includes('bundle 自带'), got.text)
|
|
136
|
+
assert.ok(got.value.entities.length >= 2, JSON.stringify(got.value.entities))
|
|
137
|
+
})
|
|
138
|
+
const missing = await call('memory_get', { id: 'c-20260101-0000' })
|
|
139
|
+
check('get on a missing id says so', () => {
|
|
140
|
+
assert.equal(missing.value.found, false)
|
|
141
|
+
assert.ok(missing.text.includes('No card'))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
/* ------------------------------------------------------------------ timeline */
|
|
145
|
+
const line = await call('memory_timeline', { entity: 'dsh插件' })
|
|
146
|
+
check('timeline returns the matching card', () => {
|
|
147
|
+
assert.ok(line.value.count >= 1, line.text)
|
|
148
|
+
assert.ok(line.text.includes('bundle'), line.text)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/* ------------------------------------------------------------------ graph */
|
|
152
|
+
const graph = await call('memory_graph', { node: 'dsh插件' })
|
|
153
|
+
check('graph walks from an entity to its memories', () => {
|
|
154
|
+
assert.equal(graph.value.found, true)
|
|
155
|
+
assert.ok(graph.value.memories.length >= 1, graph.text)
|
|
156
|
+
assert.ok(graph.text.includes('memories'), graph.text)
|
|
157
|
+
})
|
|
158
|
+
const graphFromCard = await call('memory_graph', { node: remembered.value.id })
|
|
159
|
+
check('graph accepts a card id as the seed', () => {
|
|
160
|
+
assert.equal(graphFromCard.value.found, true)
|
|
161
|
+
assert.ok(graphFromCard.text.includes(remembered.value.id), graphFromCard.text)
|
|
162
|
+
})
|
|
163
|
+
check('graph names the neighbouring entities', () => {
|
|
164
|
+
assert.ok(graph.value.neighbours.length >= 1, JSON.stringify(graph.value.neighbours))
|
|
165
|
+
assert.ok(graph.text.includes('neighbours'), graph.text)
|
|
166
|
+
})
|
|
167
|
+
const graphMiss = await call('memory_graph', { node: '完全不存在的实体zzz' })
|
|
168
|
+
check('graph on an unknown node says so', () => {
|
|
169
|
+
assert.equal(graphMiss.value.found, false)
|
|
170
|
+
assert.ok(graphMiss.text.includes('Nothing in memory'), graphMiss.text)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
/* ------------------------------------------------------------------ distill */
|
|
174
|
+
const distilled = await call('memory_distill', {})
|
|
175
|
+
check('distill without an extractor explains itself instead of failing', () => {
|
|
176
|
+
assert.equal(distilled.value.ok, false)
|
|
177
|
+
assert.ok(distilled.text.includes('extractor.enabled'), distilled.text)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// The plugin wires one runner and hands it to the tools, the route and the
|
|
181
|
+
// automatic distiller. The tool must use it: a distillation the model asks for
|
|
182
|
+
// while a scheduled one is already running has to be refused, not run twice.
|
|
183
|
+
const runnerTools = []
|
|
184
|
+
const runnerCalls = []
|
|
185
|
+
registerTools(
|
|
186
|
+
{ tools: { register: (definition) => (runnerTools.push(definition), () => {}) }, get: () => undefined, logger: { info() {}, warn() {} } },
|
|
187
|
+
{
|
|
188
|
+
cfg,
|
|
189
|
+
vault,
|
|
190
|
+
redactor,
|
|
191
|
+
runner: {
|
|
192
|
+
async run(options) {
|
|
193
|
+
runnerCalls.push(options)
|
|
194
|
+
return { ok: true, episodes: 2, written: 1, cards: [{ id: 'c9', kind: 'fact', title: '走 runner 写的卡' }] }
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
const viaRunner = await runnerTools.find((d) => d.name === 'memory_distill').execute({ project: 'tools-test' }, { signal: 'SIG' })
|
|
200
|
+
check('the model’s distillation goes through the shared runner, not around it', () => {
|
|
201
|
+
assert.equal(runnerCalls.length, 1, 'exactly one run')
|
|
202
|
+
assert.equal(runnerCalls[0].project, 'tools-test', 'the tool arg survives the hop')
|
|
203
|
+
assert.equal(runnerCalls[0].dryRun, false)
|
|
204
|
+
assert.equal(runnerCalls[0].signal, 'SIG', 'cancellation survives it too')
|
|
205
|
+
assert.equal(viaRunner.ok, true)
|
|
206
|
+
assert.ok(viaRunner.text.includes('走 runner 写的卡'), viaRunner.text)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
/* ------------------------------------------------------------------ forget */
|
|
210
|
+
const retracted = await call('memory_forget', { id: secret.value.id, mode: 'retract', reason: 'test' })
|
|
211
|
+
check('retract keeps the card and marks it', () => {
|
|
212
|
+
assert.equal(retracted.value.ok, true)
|
|
213
|
+
assert.ok(retracted.text.includes('Retracted'), retracted.text)
|
|
214
|
+
})
|
|
215
|
+
const purged = await call('memory_forget', { id: secret.value.id, mode: 'purge' })
|
|
216
|
+
check('purge removes it for good', () => {
|
|
217
|
+
assert.equal(purged.value.ok, true)
|
|
218
|
+
})
|
|
219
|
+
const goneAfter = await vault.getCard(secret.value.id)
|
|
220
|
+
check('purged card is gone from the vault', () => {
|
|
221
|
+
assert.equal(goneAfter, null)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
/* ------------------------------------------------------------------ stats */
|
|
225
|
+
const stats = await call('memory_stats', {})
|
|
226
|
+
check('stats reports the vault and the counts', () => {
|
|
227
|
+
assert.ok(stats.text.includes(dir), stats.text)
|
|
228
|
+
assert.ok(stats.text.includes('privacy rules'), stats.text)
|
|
229
|
+
assert.equal(stats.value.cards.total >= 1, true)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
console.log(results.join('\n'))
|
|
233
|
+
if (process.exitCode) {
|
|
234
|
+
console.log('\nSOME CHECKS FAILED')
|
|
235
|
+
} else {
|
|
236
|
+
console.log(`\nall ${results.length} checks passed`)
|
|
237
|
+
await fsp.rm(dir, { recursive: true, force: true })
|
|
238
|
+
}
|