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,272 @@
|
|
|
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 { registerHooks } from 'node:module'
|
|
7
|
+
|
|
8
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-backend-compensation-'))
|
|
9
|
+
process.env.DSH_MEMU_HOME = home
|
|
10
|
+
process.env.DSH_MEMU_MEMORY_HOME = home
|
|
11
|
+
|
|
12
|
+
const calls = []
|
|
13
|
+
let commitHook = null
|
|
14
|
+
let commitResult = null
|
|
15
|
+
let forgetHook = null
|
|
16
|
+
let vp
|
|
17
|
+
|
|
18
|
+
const backend = {
|
|
19
|
+
async commit(args) {
|
|
20
|
+
calls.push({ op: 'commit', ...args })
|
|
21
|
+
if (commitHook) await commitHook(args)
|
|
22
|
+
return commitResult || { ok: true }
|
|
23
|
+
},
|
|
24
|
+
async retrieve(args) {
|
|
25
|
+
calls.push({ op: 'retrieve', ...args })
|
|
26
|
+
return { files: [], segments: [], resources: [], raw: null }
|
|
27
|
+
},
|
|
28
|
+
async forget(args) {
|
|
29
|
+
calls.push({ op: 'forget', ...args })
|
|
30
|
+
if (forgetHook) return forgetHook(args)
|
|
31
|
+
return { deleted: 1 }
|
|
32
|
+
},
|
|
33
|
+
async listFiles() {
|
|
34
|
+
throw new Error('unexpected list engine call')
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
globalThis.__backendCompensationTest = backend
|
|
39
|
+
const engineSource = 'const bridge = globalThis.__backendCompensationTest; export const commit = bridge.commit, retrieve = bridge.retrieve, forget = bridge.forget, listFiles = bridge.listFiles;'
|
|
40
|
+
const hooks = registerHooks({
|
|
41
|
+
resolve(specifier, context, nextResolve) {
|
|
42
|
+
if (specifier === './memu.js') {
|
|
43
|
+
return { url: 'data:text/javascript,' + encodeURIComponent(engineSource), shortCircuit: true }
|
|
44
|
+
}
|
|
45
|
+
return nextResolve(specifier, context)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const state = await import('../lib/state.js')
|
|
50
|
+
vp = await import('../lib/vector-profile.js')
|
|
51
|
+
const paths = await import('../lib/paths.js')
|
|
52
|
+
const memory = await import('../lib/memory.js')
|
|
53
|
+
const profile = vp.profileFromCatalog('bge-small-zh-v1.5-int8')
|
|
54
|
+
const activeFile = vp.vectorActiveFile()
|
|
55
|
+
let serial = 0
|
|
56
|
+
|
|
57
|
+
function entry(content, extra = {}) {
|
|
58
|
+
return {
|
|
59
|
+
status: 'active',
|
|
60
|
+
title: content,
|
|
61
|
+
content,
|
|
62
|
+
layer: 'long_term',
|
|
63
|
+
strength: 1,
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
updatedAt: new Date().toISOString(),
|
|
66
|
+
...extra,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fixture(entries = {}) {
|
|
71
|
+
serial += 1
|
|
72
|
+
const sessionId = 'backend-compensation-' + serial
|
|
73
|
+
const cwd = path.join(home, 'project-' + serial)
|
|
74
|
+
const binding = state.ensureSession(sessionId, { cwd, agentPreset: 'coding' })
|
|
75
|
+
state.setSessionBinding(sessionId, { useLibrary: true, useGlobal: true, domainOverride: 'coding' })
|
|
76
|
+
state.savePrivateIndex(binding.libraryId, { entries })
|
|
77
|
+
return {
|
|
78
|
+
sessionId,
|
|
79
|
+
libraryId: binding.libraryId,
|
|
80
|
+
exec: { agent: { session: { header: { id: sessionId, cwd, agentPreset: 'coding' } } } },
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function activate(libraryIds = []) {
|
|
85
|
+
const databases = vp.buildDatabaseSet({ profile, generation: 1, libraryIds })
|
|
86
|
+
fs.mkdirSync(path.join(home, 'libraries'), { recursive: true })
|
|
87
|
+
for (const row of [databases.global, ...Object.values(databases.libraries)]) {
|
|
88
|
+
fs.writeFileSync(vp.resolveVectorPath(row.path), '')
|
|
89
|
+
}
|
|
90
|
+
vp.updateVectorStateSync(current => ({
|
|
91
|
+
...current,
|
|
92
|
+
generation: 1,
|
|
93
|
+
requested: null,
|
|
94
|
+
pending: null,
|
|
95
|
+
active: { status: 'active', generation: 1, fingerprint: profile.fingerprint, profile, databases },
|
|
96
|
+
}))
|
|
97
|
+
return databases
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test.beforeEach(() => {
|
|
101
|
+
fs.rmSync(activeFile, { force: true })
|
|
102
|
+
state.updateSettings({ memoryBackend: 'api', vectorSource: 'local', relevanceThreshold: 0 })
|
|
103
|
+
state.saveGlobalIndex({ entries: {} })
|
|
104
|
+
calls.length = 0
|
|
105
|
+
commitHook = null
|
|
106
|
+
commitResult = null
|
|
107
|
+
forgetHook = null
|
|
108
|
+
paths.setWarnSink(() => {})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test.after(() => {
|
|
112
|
+
hooks.deregister()
|
|
113
|
+
delete globalThis.__backendCompensationTest
|
|
114
|
+
paths.setWarnSink(null)
|
|
115
|
+
fs.rmSync(home, { recursive: true, force: true })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('remember commits a real backend row, then forgets it after a failed index CAS', async () => {
|
|
119
|
+
const f = fixture()
|
|
120
|
+
activate([f.libraryId])
|
|
121
|
+
commitHook = async () => state.setSessionBinding(f.sessionId, { libraryId: 'rebound-library' })
|
|
122
|
+
|
|
123
|
+
await assert.rejects(
|
|
124
|
+
memory.privateRemember({ exec: f.exec, content: 'new backend fact', title: 'new backend title' }),
|
|
125
|
+
error => error?.message === 'memory scope changed during write; retry',
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert.deepEqual(calls.map(call => call.op), ['commit', 'forget'])
|
|
129
|
+
assert.equal(calls[0].route, 'managed-local')
|
|
130
|
+
assert.equal(calls[0].db, vp.dbPathFor(f.libraryId))
|
|
131
|
+
assert.equal(calls[0].payload.recall_files[0].name.startsWith('mem-'), true)
|
|
132
|
+
assert.equal(calls[0].payload.recall_files[0].content, 'new backend fact')
|
|
133
|
+
assert.deepEqual(calls[1], { op: 'forget', route: 'managed-local', db: vp.dbPathFor(f.libraryId), name: calls[0].payload.recall_files[0].name })
|
|
134
|
+
assert.deepEqual(state.loadPrivateIndex(f.libraryId).entries, {})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('update commits a new row, then forgets and restores the authoritative old payload', async () => {
|
|
138
|
+
const old = entry('old authoritative fact', { title: 'old authoritative title', track: 'habit' })
|
|
139
|
+
const f = fixture({ 'mem-live': old })
|
|
140
|
+
activate([f.libraryId])
|
|
141
|
+
commitHook = async () => state.setSessionBinding(f.sessionId, { libraryId: 'rebound-library' })
|
|
142
|
+
|
|
143
|
+
await assert.rejects(
|
|
144
|
+
memory.privateUpdate({ exec: f.exec, id: 'mem-live', content: 'new backend fact', title: 'new backend title' }),
|
|
145
|
+
error => error?.message === 'memory scope changed during update; retry',
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(calls.map(call => call.op), ['commit', 'forget', 'commit'])
|
|
149
|
+
assert.equal(calls[0].payload.recall_files[0].content, 'new backend fact')
|
|
150
|
+
assert.equal(calls[0].payload.recall_files[0].description, 'new backend title')
|
|
151
|
+
assert.deepEqual(calls[1], { op: 'forget', route: 'managed-local', db: vp.dbPathFor(f.libraryId), name: 'mem-live' })
|
|
152
|
+
assert.equal(calls[2].route, 'managed-local')
|
|
153
|
+
assert.equal(calls[2].db, vp.dbPathFor(f.libraryId))
|
|
154
|
+
assert.deepEqual(calls[2].payload.recall_files[0], {
|
|
155
|
+
name: 'mem-live',
|
|
156
|
+
track: 'habit',
|
|
157
|
+
description: 'old authoritative title',
|
|
158
|
+
content: 'old authoritative fact',
|
|
159
|
+
})
|
|
160
|
+
assert.deepEqual(state.loadPrivateIndex(f.libraryId).entries['mem-live'], old)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('uncertain delete acknowledgement exposes compensation failure and preserves the original CAS cause', async () => {
|
|
164
|
+
const f = fixture()
|
|
165
|
+
activate([f.libraryId])
|
|
166
|
+
commitHook = async () => state.setSessionBinding(f.sessionId, { libraryId: 'rebound-library' })
|
|
167
|
+
forgetHook = () => ({ ok: false })
|
|
168
|
+
|
|
169
|
+
await assert.rejects(
|
|
170
|
+
memory.privateRemember({ exec: f.exec, content: 'delete acknowledgement is uncertain' }),
|
|
171
|
+
error => error?.code === 'PRIVATE_BACKEND_COMPENSATION_FAILED'
|
|
172
|
+
&& /backend compensation delete was not acknowledged/.test(error.message)
|
|
173
|
+
&& error.cause?.message === 'memory scope changed during write; retry',
|
|
174
|
+
)
|
|
175
|
+
assert.deepEqual(calls.map(call => call.op), ['commit', 'forget'])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('uncertain restore acknowledgement exposes compensation failure and preserves the original CAS cause', async () => {
|
|
179
|
+
const old = entry('restore me', { title: 'restore title' })
|
|
180
|
+
const f = fixture({ 'mem-live': old })
|
|
181
|
+
activate([f.libraryId])
|
|
182
|
+
let commits = 0
|
|
183
|
+
commitHook = async () => {
|
|
184
|
+
commits += 1
|
|
185
|
+
if (commits === 1) state.setSessionBinding(f.sessionId, { libraryId: 'rebound-library' })
|
|
186
|
+
else commitResult = { parseWarning: 'restore response was not parseable' }
|
|
187
|
+
}
|
|
188
|
+
forgetHook = () => ({ deleted: 1 })
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await assert.rejects(
|
|
192
|
+
memory.privateUpdate({ exec: f.exec, id: 'mem-live', content: 'replacement' }),
|
|
193
|
+
error => error?.code === 'PRIVATE_BACKEND_COMPENSATION_FAILED'
|
|
194
|
+
&& /backend compensation restore was not acknowledged/.test(error.message)
|
|
195
|
+
&& error.cause?.message === 'memory scope changed during update; retry',
|
|
196
|
+
)
|
|
197
|
+
} finally {
|
|
198
|
+
commitResult = null
|
|
199
|
+
}
|
|
200
|
+
assert.deepEqual(calls.map(call => call.op), ['commit', 'forget', 'commit'])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('metadata changed before compensation refuses repair instead of weakening the expectedEntry guard', async () => {
|
|
204
|
+
const old = entry('authoritative body', { title: 'authoritative title' })
|
|
205
|
+
const f = fixture({ 'mem-live': old })
|
|
206
|
+
activate([f.libraryId])
|
|
207
|
+
commitHook = async () => {
|
|
208
|
+
state.updatePrivateIndex(f.libraryId, fresh => {
|
|
209
|
+
fresh.entries['mem-live'].title = 'concurrent metadata winner'
|
|
210
|
+
})
|
|
211
|
+
state.setSessionBinding(f.sessionId, { libraryId: 'rebound-library' })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await assert.rejects(
|
|
215
|
+
memory.privateUpdate({ exec: f.exec, id: 'mem-live', content: 'replacement' }),
|
|
216
|
+
error => error?.code === 'PRIVATE_BACKEND_COMPENSATION_FAILED'
|
|
217
|
+
&& /memory changed before backend compensation/.test(error.message)
|
|
218
|
+
&& error.cause?.message === 'memory changed concurrently; reload and retry',
|
|
219
|
+
)
|
|
220
|
+
assert.deepEqual(calls.map(call => call.op), ['commit'])
|
|
221
|
+
assert.equal(state.loadPrivateIndex(f.libraryId).entries['mem-live'].title, 'concurrent metadata winner')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('same-library managed writes are serialized through the backend queue', async () => {
|
|
225
|
+
const f = fixture()
|
|
226
|
+
activate([f.libraryId])
|
|
227
|
+
let active = 0
|
|
228
|
+
let maximum = 0
|
|
229
|
+
let commits = 0
|
|
230
|
+
let entered
|
|
231
|
+
let release
|
|
232
|
+
const firstEntered = new Promise(resolve => { entered = resolve })
|
|
233
|
+
const gate = new Promise(resolve => { release = resolve })
|
|
234
|
+
commitHook = async () => {
|
|
235
|
+
commits += 1
|
|
236
|
+
active += 1
|
|
237
|
+
maximum = Math.max(maximum, active)
|
|
238
|
+
if (commits === 1) {
|
|
239
|
+
entered()
|
|
240
|
+
await gate
|
|
241
|
+
}
|
|
242
|
+
active -= 1
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const first = memory.privateRemember({ exec: f.exec, content: 'first queued fact' })
|
|
246
|
+
await firstEntered
|
|
247
|
+
const second = memory.privateRemember({ exec: f.exec, content: 'second queued fact' })
|
|
248
|
+
await Promise.resolve()
|
|
249
|
+
assert.equal(calls.filter(call => call.op === 'commit').length, 1)
|
|
250
|
+
release()
|
|
251
|
+
await Promise.all([first, second])
|
|
252
|
+
assert.equal(maximum, 1)
|
|
253
|
+
assert.equal(calls.filter(call => call.op === 'commit').length, 2)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('a rejected write does not poison the same-library queue', async () => {
|
|
257
|
+
const f = fixture()
|
|
258
|
+
activate([f.libraryId])
|
|
259
|
+
let commits = 0
|
|
260
|
+
commitHook = async () => {
|
|
261
|
+
commits += 1
|
|
262
|
+
if (commits === 1) state.setSessionBinding(f.sessionId, { libraryId: 'rebound-library' })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await assert.rejects(memory.privateRemember({ exec: f.exec, content: 'first rejected fact' }), /memory scope changed during write; retry/)
|
|
266
|
+
state.setSessionBinding(f.sessionId, { libraryId: f.libraryId, useLibrary: true, useGlobal: true })
|
|
267
|
+
const second = await memory.privateRemember({ exec: f.exec, content: 'second healthy fact' })
|
|
268
|
+
|
|
269
|
+
assert.ok(second.id)
|
|
270
|
+
assert.deepEqual(calls.map(call => call.op), ['commit', 'forget', 'commit'])
|
|
271
|
+
assert.equal(state.loadPrivateIndex(f.libraryId).entries[second.id].content, 'second healthy fact')
|
|
272
|
+
})
|