dsh-memu-memory 1.1.3 → 1.2.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/AGENTS.md +1 -1
- package/README.md +31 -2
- package/README.zh.md +30 -2
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +1 -1
- package/docs/emotion-model-spec.md +281 -0
- package/docs/host-api.md +2 -2
- package/docs/memory-semantics.md +12 -10
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +395 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +1 -1
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +33 -11
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +115 -0
- package/lib/cycle.js +40 -6
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +221 -19
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +335 -40
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +9 -1
- package/lib/state.js +29 -12
- package/lib/tools.js +49 -2
- package/lib/vector-profile.js +27 -0
- package/package.json +2 -1
- package/scripts/acceptance-local.mjs +2 -1
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +11 -1
- package/scripts/check-all.mjs +10 -1
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +212 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +320 -0
- package/tests/consolidation-fork.test.mjs +357 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +321 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +77 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +92 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +536 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +208 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +733 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +164 -0
- package/tests/windows-env.test.mjs +89 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-memory-embed-runtime-'))
|
|
9
|
+
process.env.DSH_MEMU_HOME = home
|
|
10
|
+
delete process.env.DSH_MEMU_EMBED_RUNTIME_DIR
|
|
11
|
+
|
|
12
|
+
const runtime = await import('../lib/embedding-runtime.js')
|
|
13
|
+
const { getEmbeddingModel } = await import('../lib/embedding-catalog.js')
|
|
14
|
+
const service = await import('../lib/local-vector-service.js')
|
|
15
|
+
const { profileFromCatalog, setRequestedProfile } = await import('../lib/vector-profile.js')
|
|
16
|
+
|
|
17
|
+
test.after(() => fs.rmSync(home, { recursive: true, force: true }))
|
|
18
|
+
|
|
19
|
+
const sha256 = value => crypto.createHash('sha256').update(value).digest('hex')
|
|
20
|
+
const utf8 = text => Buffer.from(text, 'utf8')
|
|
21
|
+
|
|
22
|
+
// A fake registry payload: the pinned facts below are the test's own, computed from the
|
|
23
|
+
// same bytes, so this fixture proves the *mechanism* (pin, verify, extract, smoke) rather
|
|
24
|
+
// than pretending to be a real artifact. Nothing here is ever executed.
|
|
25
|
+
const PKG_JSON = utf8('{"name":"onnxruntime-node","version":"9.9.9"}')
|
|
26
|
+
const NATIVE_BYTES = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x66, 0x61, 0x6b, 0x65])
|
|
27
|
+
const JS_BYTES = utf8('module.exports = 42\n')
|
|
28
|
+
const TARBALL = Buffer.from('fake pinned runtime archive')
|
|
29
|
+
|
|
30
|
+
function fixtureEntry(overrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
id: 'fixture-model',
|
|
33
|
+
availability: 'available',
|
|
34
|
+
runtime: {
|
|
35
|
+
engine: 'onnxruntime-node',
|
|
36
|
+
version: '9.9.9',
|
|
37
|
+
abi: 'napi-v6',
|
|
38
|
+
os: 'win32',
|
|
39
|
+
arch: 'x64',
|
|
40
|
+
artifacts: [
|
|
41
|
+
{
|
|
42
|
+
name: 'onnxruntime-node',
|
|
43
|
+
version: '9.9.9',
|
|
44
|
+
url: 'https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-9.9.9.tgz',
|
|
45
|
+
bytes: TARBALL.length,
|
|
46
|
+
sha256: sha256(TARBALL),
|
|
47
|
+
extract: ['package.json', 'lib', 'bin/napi-v6/<platform>'],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
packageFiles: [{ package: 'onnxruntime-node', bytes: PKG_JSON.length, sha256: sha256(PKG_JSON) }],
|
|
51
|
+
native: {
|
|
52
|
+
'win32-x64': [
|
|
53
|
+
{
|
|
54
|
+
path: 'node_modules/onnxruntime-node/bin/napi-v6/win32/x64/onnxruntime_binding.node',
|
|
55
|
+
bytes: NATIVE_BYTES.length,
|
|
56
|
+
sha256: sha256(NATIVE_BYTES),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
...overrides,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fakeExtract({ cwd }) {
|
|
66
|
+
fs.writeFileSync(path.join(cwd, 'package.json'), PKG_JSON)
|
|
67
|
+
fs.mkdirSync(path.join(cwd, 'lib'), { recursive: true })
|
|
68
|
+
fs.writeFileSync(path.join(cwd, 'lib', 'index.js'), JS_BYTES)
|
|
69
|
+
const dir = path.join(cwd, 'bin', 'napi-v6', 'win32', 'x64')
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
71
|
+
fs.writeFileSync(path.join(dir, 'onnxruntime_binding.node'), NATIVE_BYTES)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fakeFetch({ bytes = TARBALL, calls = [] } = {}) {
|
|
75
|
+
const fetchImpl = async url => {
|
|
76
|
+
calls.push(String(url))
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
status: 200,
|
|
80
|
+
headers: { get: () => null },
|
|
81
|
+
body: (async function* () { yield bytes })(),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { fetchImpl, calls }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const okSmoke = async () => ({ ok: true, ortVersion: '9.9.9', tokenizer: true, checks: ['smoke:fixture'] })
|
|
88
|
+
|
|
89
|
+
function makeInstaller(root, options = {}) {
|
|
90
|
+
const entry = options.entry || fixtureEntry()
|
|
91
|
+
const fetch = options.fetch || fakeFetch()
|
|
92
|
+
return {
|
|
93
|
+
entry,
|
|
94
|
+
fetch,
|
|
95
|
+
installer: runtime.createEmbeddingRuntimeInstaller({
|
|
96
|
+
root,
|
|
97
|
+
platform: 'win32-x64',
|
|
98
|
+
resolveEntry: () => entry,
|
|
99
|
+
fetchImpl: fetch.fetchImpl,
|
|
100
|
+
extract: options.extract === null ? undefined : (options.extract || fakeExtract),
|
|
101
|
+
smoke: options.smoke || okSmoke,
|
|
102
|
+
logger: { warn: () => {} },
|
|
103
|
+
}),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test('a pending catalog model pins no runtime and is refused before any download', () => {
|
|
108
|
+
const plan = runtime.runtimePlan(getEmbeddingModel('all-minilm-l6-v2-int8'), 'win32-x64')
|
|
109
|
+
assert.equal(plan.ok, false)
|
|
110
|
+
assert.ok(plan.errors.some(item => item.code === 'RUNTIME_ENTRY_NOT_AVAILABLE'))
|
|
111
|
+
assert.ok(plan.errors.some(item => item.code === 'RUNTIME_ARTIFACTS_MISSING'))
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('a platform with no pinned native binaries fails closed instead of guessing', () => {
|
|
115
|
+
const entry = getEmbeddingModel('bge-small-zh-v1.5-int8')
|
|
116
|
+
const plan = runtime.runtimePlan(entry, 'darwin-x64')
|
|
117
|
+
assert.equal(plan.ok, false)
|
|
118
|
+
assert.deepEqual(plan.errors.map(item => item.code), ['RUNTIME_NATIVE_MISSING'])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('the verified BGE entry pins a complete runtime for every shipped platform', () => {
|
|
122
|
+
const entry = getEmbeddingModel('bge-small-zh-v1.5-int8')
|
|
123
|
+
for (const platform of ['win32-x64', 'win32-arm64', 'linux-x64', 'linux-arm64', 'darwin-arm64']) {
|
|
124
|
+
const plan = runtime.runtimePlan(entry, platform)
|
|
125
|
+
assert.equal(plan.ok, true, platform + ': ' + JSON.stringify(plan.errors))
|
|
126
|
+
assert.equal(plan.artifacts.length, 3)
|
|
127
|
+
for (const artifact of plan.artifacts) {
|
|
128
|
+
assert.match(artifact.sha256, /^[0-9a-f]{64}$/)
|
|
129
|
+
assert.equal(artifact.url.startsWith('https://'), true)
|
|
130
|
+
assert.ok(artifact.bytes > 0)
|
|
131
|
+
}
|
|
132
|
+
assert.ok(plan.native.length >= 2)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('guessed or missing hashes are rejected: no artifact installs without bytes and sha256', () => {
|
|
137
|
+
const missingHash = runtime.runtimePlan(fixtureEntry({ version: null }), 'win32-x64')
|
|
138
|
+
assert.equal(missingHash.ok, false)
|
|
139
|
+
assert.ok(missingHash.errors.some(item => item.code === 'RUNTIME_VERSION_UNPINNED'))
|
|
140
|
+
|
|
141
|
+
const guessed = fixtureEntry()
|
|
142
|
+
guessed.runtime.artifacts[0].sha256 = 'deadbeef'
|
|
143
|
+
const plan = runtime.runtimePlan(guessed, 'win32-x64')
|
|
144
|
+
assert.equal(plan.ok, false)
|
|
145
|
+
assert.ok(plan.errors.some(item => item.code === 'RUNTIME_ARTIFACT_SHA256_INVALID'))
|
|
146
|
+
|
|
147
|
+
const noBytes = fixtureEntry()
|
|
148
|
+
noBytes.runtime.artifacts[0].bytes = 0
|
|
149
|
+
assert.ok(runtime.runtimePlan(noBytes, 'win32-x64').errors.some(item => item.code === 'RUNTIME_ARTIFACT_BYTES_INVALID'))
|
|
150
|
+
|
|
151
|
+
assert.throws(() => runtime.verifyRuntimeArtifact(TARBALL, { name: 'x', bytes: TARBALL.length }), /no pinned sha256/)
|
|
152
|
+
assert.throws(() => runtime.verifyRuntimeArtifact(TARBALL, { name: 'x', bytes: TARBALL.length, sha256: '0'.repeat(64) }), /hash_mismatch|does not match the pinned/i)
|
|
153
|
+
assert.throws(() => runtime.verifyRuntimeArtifact(TARBALL, { name: 'x', bytes: TARBALL.length + 1, sha256: sha256(TARBALL) }), /expects|expected/i)
|
|
154
|
+
assert.equal(runtime.verifyRuntimeArtifact(TARBALL, { name: 'x', bytes: TARBALL.length, sha256: sha256(TARBALL) }), sha256(TARBALL))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('the extraction allow-list carries <platform> and never widens to another platform', () => {
|
|
158
|
+
const entry = getEmbeddingModel('bge-small-zh-v1.5-int8')
|
|
159
|
+
const plan = runtime.runtimePlan(entry, 'win32-x64')
|
|
160
|
+
const list = runtime.extractAllowList(plan.artifacts[0], 'win32-x64')
|
|
161
|
+
assert.ok(list.includes('bin/napi-v6/win32/x64'))
|
|
162
|
+
assert.equal(list.some(item => item.includes('<platform>')), false)
|
|
163
|
+
const allows = rel => list.some(prefix => rel === prefix || rel.startsWith(prefix + '/'))
|
|
164
|
+
assert.equal(allows('bin/napi-v6/win32/x64/onnxruntime.dll'), true)
|
|
165
|
+
assert.equal(allows('bin/napi-v6/linux/x64/libonnxruntime.so.1'), false)
|
|
166
|
+
assert.equal(allows('script/install.js'), false)
|
|
167
|
+
assert.equal(allows('package.json'), true)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('install needs explicit confirmation and never touches the network before it', async () => {
|
|
171
|
+
const root = path.join(home, 'no-confirm')
|
|
172
|
+
const { installer, fetch } = makeInstaller(root)
|
|
173
|
+
await assert.rejects(installer.ensure({}), /confirmation|explicit/i)
|
|
174
|
+
assert.deepEqual(fetch.calls, [])
|
|
175
|
+
assert.equal(installer.getStatus().status, 'absent')
|
|
176
|
+
assert.equal(fs.existsSync(path.join(root, 'active.json')), false)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('install verifies bytes, extracts, smokes and publishes an atomic active pointer', async () => {
|
|
180
|
+
const root = path.join(home, 'happy')
|
|
181
|
+
const { installer, fetch } = makeInstaller(root)
|
|
182
|
+
const result = await installer.ensure({ confirmed: true })
|
|
183
|
+
assert.equal(result.status, 'installed')
|
|
184
|
+
assert.equal(result.ready, true)
|
|
185
|
+
assert.equal(result.reused, false)
|
|
186
|
+
assert.equal(fetch.calls.length, 1)
|
|
187
|
+
assert.equal(fs.existsSync(path.join(result.path, 'installed.json')), true)
|
|
188
|
+
assert.equal(fs.existsSync(path.join(result.path, 'node_modules', 'onnxruntime-node', 'package.json')), true)
|
|
189
|
+
const native = path.join(result.path, 'node_modules', 'onnxruntime-node', 'bin', 'napi-v6', 'win32', 'x64', 'onnxruntime_binding.node')
|
|
190
|
+
assert.deepEqual(fs.readFileSync(native), NATIVE_BYTES)
|
|
191
|
+
const active = JSON.parse(fs.readFileSync(path.join(root, 'active.json'), 'utf8'))
|
|
192
|
+
assert.equal(active.key, result.key)
|
|
193
|
+
assert.equal(active.dir, result.path)
|
|
194
|
+
assert.equal(active.platform, 'win32-x64')
|
|
195
|
+
const record = JSON.parse(fs.readFileSync(path.join(result.path, 'installed.json'), 'utf8'))
|
|
196
|
+
assert.equal(record.postinstall, 'never-run')
|
|
197
|
+
assert.equal(record.smoke.ok, true)
|
|
198
|
+
assert.equal(record.artifacts[0].sha256, sha256(TARBALL))
|
|
199
|
+
assert.ok(record.files.some(file => file.path.endsWith('onnxruntime_binding.node')))
|
|
200
|
+
// 安装物不能留下 tarball:受管目录只放解包后的运行时。
|
|
201
|
+
assert.equal(fs.existsSync(path.join(result.path, '.artifacts')), false)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('a restart reuses the verified runtime instead of downloading 115 MB again', async () => {
|
|
205
|
+
const root = path.join(home, 'reuse')
|
|
206
|
+
const first = makeInstaller(root)
|
|
207
|
+
const installed = await first.installer.ensure({ confirmed: true })
|
|
208
|
+
assert.equal(first.fetch.calls.length, 1)
|
|
209
|
+
|
|
210
|
+
const second = makeInstaller(root)
|
|
211
|
+
assert.equal(second.installer.getStatus().status, 'installed')
|
|
212
|
+
const reused = await second.installer.ensure({})
|
|
213
|
+
assert.equal(reused.reused, true)
|
|
214
|
+
assert.equal(reused.path, installed.path)
|
|
215
|
+
assert.deepEqual(second.fetch.calls, [])
|
|
216
|
+
const deep = second.installer.getStatus({ deep: true })
|
|
217
|
+
assert.equal(deep.status, 'installed')
|
|
218
|
+
assert.equal(deep.ready, true)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('concurrent first-use installs share one download and stay idempotent', async () => {
|
|
222
|
+
const root = path.join(home, 'concurrent')
|
|
223
|
+
const { installer, fetch } = makeInstaller(root)
|
|
224
|
+
const [a, b] = await Promise.all([installer.ensure({ confirmed: true }), installer.ensure({ confirmed: true })])
|
|
225
|
+
assert.equal(a.path, b.path)
|
|
226
|
+
assert.equal(fetch.calls.length, 1)
|
|
227
|
+
const again = await installer.ensure({})
|
|
228
|
+
assert.equal(again.reused, true)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('a live install lock from another process is reported, not fought over', async () => {
|
|
232
|
+
const root = path.join(home, 'locked')
|
|
233
|
+
const staging = path.join(root, '.staging')
|
|
234
|
+
fs.mkdirSync(staging, { recursive: true })
|
|
235
|
+
fs.writeFileSync(path.join(staging, 'install.lock'), JSON.stringify({ pid: process.pid, at: Date.now() }))
|
|
236
|
+
const { installer, fetch } = makeInstaller(root)
|
|
237
|
+
await assert.rejects(installer.ensure({ confirmed: true }), /already installing|BUSY|another process/i)
|
|
238
|
+
assert.deepEqual(fetch.calls, [])
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('a hash mismatch leaves no active pointer and no staging leftovers', async () => {
|
|
242
|
+
const root = path.join(home, 'bad-hash')
|
|
243
|
+
// 同长度、不同内容:这样命中的是 sha256 校验,而不是长度校验。
|
|
244
|
+
const tampered = Buffer.from('FAKE PINNED RUNTIME ARCHIVE')
|
|
245
|
+
const { installer, fetch } = makeInstaller(root, { fetch: fakeFetch({ bytes: tampered }) })
|
|
246
|
+
assert.equal(tampered.length, TARBALL.length)
|
|
247
|
+
await assert.rejects(installer.ensure({ confirmed: true }), /hash|sha256|checksum/i)
|
|
248
|
+
assert.equal(fetch.calls.length, 1)
|
|
249
|
+
assert.equal(fs.existsSync(path.join(root, 'active.json')), false)
|
|
250
|
+
assert.equal(installer.getStatus().status, 'failed')
|
|
251
|
+
const leftovers = fs.existsSync(path.join(root, '.staging')) ? fs.readdirSync(path.join(root, '.staging')).filter(name => name.startsWith('install-')) : []
|
|
252
|
+
assert.deepEqual(leftovers, [])
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('a native smoke failure never becomes an installed runtime', async () => {
|
|
256
|
+
const root = path.join(home, 'smoke-fail')
|
|
257
|
+
const { installer } = makeInstaller(root, { smoke: async () => ({ ok: false, message: 'cannot load onnxruntime_binding.node' }) })
|
|
258
|
+
await assert.rejects(installer.ensure({ confirmed: true }), /cannot load onnxruntime_binding.node/)
|
|
259
|
+
assert.equal(fs.existsSync(path.join(root, 'active.json')), false)
|
|
260
|
+
const status = installer.getStatus()
|
|
261
|
+
assert.equal(status.status, 'failed')
|
|
262
|
+
assert.match(status.error.message, /cannot load/)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('a tampered runtime on disk is not reused as ready', async () => {
|
|
266
|
+
const root = path.join(home, 'tamper')
|
|
267
|
+
const { installer, fetch } = makeInstaller(root)
|
|
268
|
+
const installed = await installer.ensure({ confirmed: true })
|
|
269
|
+
const native = path.join(installed.path, 'node_modules', 'onnxruntime-node', 'bin', 'napi-v6', 'win32', 'x64', 'onnxruntime_binding.node')
|
|
270
|
+
fs.writeFileSync(native, Buffer.from([1, 2, 3]))
|
|
271
|
+
assert.equal(installer.getStatus().status, 'absent')
|
|
272
|
+
await assert.rejects(installer.ensure({ reuseOnly: true }), /not installed yet/)
|
|
273
|
+
const repair = await installer.ensure({ confirmed: true })
|
|
274
|
+
assert.equal(repair.status, 'installed')
|
|
275
|
+
assert.equal(fetch.calls.length, 2)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('the real tarball extractor drops everything the allow-list did not pin', async () => {
|
|
279
|
+
const root = path.join(home, 'real-extract')
|
|
280
|
+
const stage = path.join(home, 'real-extract-source')
|
|
281
|
+
fs.mkdirSync(path.join(stage, 'package', 'bin', 'napi-v6', 'win32', 'x64'), { recursive: true })
|
|
282
|
+
fs.mkdirSync(path.join(stage, 'package', 'bin', 'napi-v6', 'linux', 'x64'), { recursive: true })
|
|
283
|
+
fs.mkdirSync(path.join(stage, 'package', 'script'), { recursive: true })
|
|
284
|
+
fs.writeFileSync(path.join(stage, 'package', 'package.json'), PKG_JSON)
|
|
285
|
+
fs.writeFileSync(path.join(stage, 'package', 'bin', 'napi-v6', 'win32', 'x64', 'onnxruntime_binding.node'), NATIVE_BYTES)
|
|
286
|
+
fs.writeFileSync(path.join(stage, 'package', 'bin', 'napi-v6', 'linux', 'x64', 'libonnxruntime.so.1'), Buffer.from('linux'))
|
|
287
|
+
fs.writeFileSync(path.join(stage, 'package', 'script', 'install.js'), Buffer.from('postinstall'))
|
|
288
|
+
|
|
289
|
+
const { c } = await import('tar')
|
|
290
|
+
const archive = path.join(home, 'real-extract.tgz')
|
|
291
|
+
await c({ gzip: true, file: archive, cwd: stage }, ['package'])
|
|
292
|
+
const bytes = fs.readFileSync(archive)
|
|
293
|
+
|
|
294
|
+
const entry = fixtureEntry()
|
|
295
|
+
entry.runtime.artifacts[0].bytes = bytes.length
|
|
296
|
+
entry.runtime.artifacts[0].sha256 = sha256(bytes)
|
|
297
|
+
const { installer } = makeInstaller(root, { entry, extract: null, fetch: fakeFetch({ bytes }) })
|
|
298
|
+
const installed = await installer.ensure({ confirmed: true })
|
|
299
|
+
const nodeModules = path.join(installed.path, 'node_modules', 'onnxruntime-node')
|
|
300
|
+
assert.deepEqual(fs.readFileSync(path.join(nodeModules, 'package.json')), PKG_JSON)
|
|
301
|
+
assert.deepEqual(fs.readFileSync(path.join(nodeModules, 'bin', 'napi-v6', 'win32', 'x64', 'onnxruntime_binding.node')), NATIVE_BYTES)
|
|
302
|
+
assert.equal(fs.existsSync(path.join(nodeModules, 'bin', 'napi-v6', 'linux')), false)
|
|
303
|
+
assert.equal(fs.existsSync(path.join(nodeModules, 'script')), false)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('service status reports a missing runtime truthfully and bootstrap never invents one', async () => {
|
|
307
|
+
const status = service.localVectorRuntimeStatus()
|
|
308
|
+
assert.equal(status.ready, false)
|
|
309
|
+
assert.notEqual(status.status, 'configured')
|
|
310
|
+
// 一个设置了但没有运行时的 env 目录不算 ready(不会被当成「配置好了」)。
|
|
311
|
+
const emptyDir = path.join(home, 'empty-runtime')
|
|
312
|
+
fs.mkdirSync(emptyDir, { recursive: true })
|
|
313
|
+
process.env.DSH_MEMU_EMBED_RUNTIME_DIR = emptyDir
|
|
314
|
+
try {
|
|
315
|
+
assert.equal(service.hasEmbeddingRuntimePackages(emptyDir), false)
|
|
316
|
+
const withEnv = service.localVectorRuntimeStatus()
|
|
317
|
+
assert.equal(withEnv.ready, false)
|
|
318
|
+
assert.equal(withEnv.envIgnored, true)
|
|
319
|
+
} finally {
|
|
320
|
+
delete process.env.DSH_MEMU_EMBED_RUNTIME_DIR
|
|
321
|
+
}
|
|
322
|
+
assert.equal(service.hasEmbeddingRuntimePackages(path.join(home, 'nope')), false)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('bootstrap fails closed with a lexical fallback signal instead of a cloud fallback', async () => {
|
|
326
|
+
const profile = profileFromCatalog('bge-small-zh-v1.5-int8', { modelDir: path.join(home, 'model'), runtimeDir: null })
|
|
327
|
+
setRequestedProfile(profile, { status: 'requested' })
|
|
328
|
+
await assert.rejects(service.bootstrapLocalVector({ confirmed: false }), error => {
|
|
329
|
+
assert.match(String(error?.code || ''), /^LOCAL_VECTOR_RUNTIME_/)
|
|
330
|
+
assert.equal(error?.fallback, 'lexical')
|
|
331
|
+
return true
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
test('missing exact bytes are refused by the artifact verification API', () => {
|
|
335
|
+
assert.throws(() => runtime.verifyRuntimeArtifact(TARBALL, { name: 'x', sha256: sha256(TARBALL) }), error => error.code === 'LOCAL_VECTOR_RUNTIME_UNVERIFIED')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('same-size native tampering is rejected before reuse without explicit download confirmation', async () => {
|
|
339
|
+
const root = path.join(home, 'same-size-tamper')
|
|
340
|
+
const first = makeInstaller(root)
|
|
341
|
+
const installed = await first.installer.ensure({ confirmed: true })
|
|
342
|
+
const native = path.join(installed.path, ...fixtureEntry().runtime.native['win32-x64'][0].path.split('/'))
|
|
343
|
+
fs.writeFileSync(native, Buffer.alloc(NATIVE_BYTES.length, 0))
|
|
344
|
+
const second = makeInstaller(root)
|
|
345
|
+
await assert.rejects(second.installer.ensure({ reuseOnly: true }), error => error.code === 'LOCAL_VECTOR_RUNTIME_ABSENT')
|
|
346
|
+
assert.deepEqual(second.fetch.calls, [])
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('an active-pointer commit failure preserves the previous generation byte-for-byte', async t => {
|
|
350
|
+
const root = path.join(home, 'atomic-pointer-fail')
|
|
351
|
+
const first = makeInstaller(root)
|
|
352
|
+
const installed = await first.installer.ensure({ confirmed: true })
|
|
353
|
+
const activeFile = path.join(root, 'active.json')
|
|
354
|
+
const activeBefore = fs.readFileSync(activeFile)
|
|
355
|
+
const native = path.join(installed.path, ...fixtureEntry().runtime.native['win32-x64'][0].path.split('/'))
|
|
356
|
+
const entry = fixtureEntry()
|
|
357
|
+
entry.runtime.artifacts[0].url += '?metadata-refresh'
|
|
358
|
+
const next = makeInstaller(root, { entry })
|
|
359
|
+
const originalRename = fs.renameSync
|
|
360
|
+
const mocked = t.mock.method(fs, 'renameSync', (source, target) => {
|
|
361
|
+
if (path.resolve(target) === path.resolve(activeFile)) throw Object.assign(new Error('simulated active pointer write failure'), { code: 'EIO' })
|
|
362
|
+
return originalRename(source, target)
|
|
363
|
+
})
|
|
364
|
+
try {
|
|
365
|
+
await assert.rejects(next.installer.ensure({ confirmed: true }), /cannot publish.*active pointer/)
|
|
366
|
+
} finally {
|
|
367
|
+
mocked.mock.restore()
|
|
368
|
+
}
|
|
369
|
+
assert.deepEqual(fs.readFileSync(activeFile), activeBefore)
|
|
370
|
+
assert.deepEqual(fs.readFileSync(native), NATIVE_BYTES)
|
|
371
|
+
const previous = makeInstaller(root)
|
|
372
|
+
assert.equal(previous.installer.getStatus({ deep: true }).ready, true)
|
|
373
|
+
const reused = await previous.installer.ensure({ reuseOnly: true })
|
|
374
|
+
assert.equal(reused.path, installed.path)
|
|
375
|
+
assert.deepEqual(previous.fetch.calls, [])
|
|
376
|
+
assert.deepEqual(fs.readdirSync(root).filter(name => name.startsWith(installed.key)), [path.basename(installed.path)])
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
test('a failed upgrade smoke preserves the previous active runtime', async () => {
|
|
380
|
+
const root = path.join(home, 'atomic-smoke-fail')
|
|
381
|
+
const first = makeInstaller(root)
|
|
382
|
+
const installed = await first.installer.ensure({ confirmed: true })
|
|
383
|
+
const activeFile = path.join(root, 'active.json')
|
|
384
|
+
const activeBefore = fs.readFileSync(activeFile)
|
|
385
|
+
const next = makeInstaller(root, { entry: fixtureEntry({ version: '9.9.10' }), smoke: async () => ({ ok: false, message: 'upgrade smoke failed' }) })
|
|
386
|
+
await assert.rejects(next.installer.ensure({ confirmed: true }), /upgrade smoke failed/)
|
|
387
|
+
assert.deepEqual(fs.readFileSync(activeFile), activeBefore)
|
|
388
|
+
assert.equal((await first.installer.ensure({ reuseOnly: true })).path, installed.path)
|
|
389
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// 情绪模型的跨模块链路:这三条不在 mood.js 的单元测试范围里,但任何一条断了都
|
|
2
|
+
// 会让情绪「写进去、存得住、展示不出」或者「整理时把当前心情当成事件情绪」。
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-emotion-chain-'))
|
|
10
|
+
process.env.DSH_MEMU_HOME = home
|
|
11
|
+
process.env.DSH_MEMU_MEMORY_HOME = home
|
|
12
|
+
|
|
13
|
+
const state = await import('../lib/state.js')
|
|
14
|
+
const memory = await import('../lib/memory.js')
|
|
15
|
+
const rpc = await import('../lib/rpc.js')
|
|
16
|
+
const remote = await import('../lib/remote-summary.js')
|
|
17
|
+
|
|
18
|
+
test.after(() => {
|
|
19
|
+
try { fs.rmSync(home, { recursive: true, force: true }) } catch {}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function setup(name) {
|
|
23
|
+
const cwd = path.join(home, 'proj-' + name)
|
|
24
|
+
const sessionId = 'chain-' + name
|
|
25
|
+
state.ensureSession(sessionId, { cwd, agentPreset: 'rp' })
|
|
26
|
+
return { cwd, sessionId, exec: { agent: { session: { header: { id: sessionId, cwd, agentPreset: 'rp' } } } } }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('the rpc projection carries the composition, the control axis and the source', async () => {
|
|
30
|
+
const { exec } = setup('projection')
|
|
31
|
+
const saved = await memory.privateRemember({
|
|
32
|
+
exec,
|
|
33
|
+
content: '他把伞朝我这边偏了偏。',
|
|
34
|
+
title: '伞',
|
|
35
|
+
emotion: { joy: 0.2, anger: 0, sorrow: 0.6, fear: 0.2 },
|
|
36
|
+
dominance: 0.55,
|
|
37
|
+
arousal: 0.72,
|
|
38
|
+
})
|
|
39
|
+
const binding = state.getSessionBinding(exec.agent.session.header.id)
|
|
40
|
+
// The wall and the drawer read through these two endpoints; a field missing here is
|
|
41
|
+
// a field the user can never see, even though it is stored correctly.
|
|
42
|
+
const listed = await rpc.dispatch(null, 'browseLibrary', { libraryId: binding.libraryId })
|
|
43
|
+
const file = (listed.files || []).find(item => item.id === saved.id)
|
|
44
|
+
assert.ok(file, 'the written entry must appear in the library listing')
|
|
45
|
+
assert.equal(Math.round(file.emotion.sorrow * 100) / 100, 0.6)
|
|
46
|
+
assert.equal(file.dominance, 0.55)
|
|
47
|
+
assert.equal(file.emotionSource, 'model')
|
|
48
|
+
|
|
49
|
+
const detail = await rpc.dispatch(null, 'getMemory', { libraryId: binding.libraryId, id: saved.id })
|
|
50
|
+
assert.equal(Math.round(detail.entry.emotion.fear * 100) / 100, 0.2)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('remote output keeps a valid composition, rejects a broken one, and invents nothing', () => {
|
|
54
|
+
const problems = []
|
|
55
|
+
const cleaned = remote.cleanMemories([
|
|
56
|
+
{ title: 'ok', content: 'a', emotion: { joy: 0, anger: 1, sorrow: 0, fear: 0 }, dominance: -0.2, arousal: 0.8 },
|
|
57
|
+
{ title: 'broken', content: 'b', emotion: { joy: 1, anger: -1, sorrow: 0, fear: 0 } },
|
|
58
|
+
{ title: 'silent', content: 'c' },
|
|
59
|
+
], problems)
|
|
60
|
+
const good = cleaned.find(item => item.title === 'ok')
|
|
61
|
+
assert.ok(good && good.emotion)
|
|
62
|
+
assert.equal(good.emotion.anger, 1)
|
|
63
|
+
assert.equal(good.dominance, -0.2)
|
|
64
|
+
// A remote model that returns an illegal share must be refused loudly: silently
|
|
65
|
+
// clamping it would put a fabricated composition into the library forever.
|
|
66
|
+
assert.equal(cleaned.some(item => item.title === 'broken'), false, 'an illegal composition is dropped, not repaired')
|
|
67
|
+
assert.ok(problems.some(problem => /emotion/.test(problem.error || '')), 'the rejection must be diagnosable')
|
|
68
|
+
// Omitted is not neutral: the entry keeps no composition rather than a made-up one.
|
|
69
|
+
const silent = cleaned.find(item => item.title === 'silent')
|
|
70
|
+
assert.ok(silent)
|
|
71
|
+
assert.equal(silent.emotion, null)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('the consolidation fork does not inherit the parent conversation mood', () => {
|
|
75
|
+
// Shape guard: the child binding is built inside requestPrivateConsolidation, which
|
|
76
|
+
// needs a live host agent to run. The one thing that must not silently disappear is
|
|
77
|
+
// the explicit `mood: null` — without it the fork inherits nothing and every entry it
|
|
78
|
+
// writes falls back to neutral, which is the exact bug this guard exists for.
|
|
79
|
+
const source = fs.readFileSync(path.join(import.meta.dirname, '..', 'lib', 'private.js'), 'utf8')
|
|
80
|
+
const start = source.indexOf('setSessionBinding(childId')
|
|
81
|
+
assert.ok(start > 0, 'the child binding block moved; update this guard')
|
|
82
|
+
const block = source.slice(start, start + 1200)
|
|
83
|
+
assert.match(block, /mood:\s*null/, 'the fork must declare "no present mood" instead of inheriting the parent one')
|
|
84
|
+
})
|