solopup-memory 0.21.0-beta.3 → 0.22.0-beta.3
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 +30 -0
- package/COMPATIBILITY.md +8 -5
- package/DETECTION.md +139 -0
- package/DREAMING-DESIGN.md +167 -0
- package/README.en.md +33 -6
- package/README.md +38 -7
- package/RELEASE.md +26 -0
- package/SECURITY.md +7 -0
- package/context-history.mjs +20 -3
- package/detect.mjs +441 -0
- package/eval/bench-detect.mjs +107 -0
- package/eval/semantic/cases.json +387 -0
- package/eval/semantic-suite.mjs +156 -0
- package/index.mjs +82 -10
- package/managed-policy.mjs +3 -1
- package/package.json +90 -69
- package/scripts/demo-source-recall.mjs +123 -0
- package/scripts/detect.mjs +69 -0
- package/scripts/dream.mjs +205 -0
- package/scripts/probe-history.mjs +92 -0
- package/scripts/probe-load.mjs +1 -1
- package/scripts/probe-package.mjs +6 -2
- package/scripts/security-check.mjs +173 -0
- package/scripts/soak.mjs +219 -0
- package/scripts/telemetry.mjs +187 -0
- package/scripts/verify.mjs +3 -1
- package/{test-adversarial.mjs → test/test-adversarial.mjs} +5 -5
- package/{test-belief-revision.mjs → test/test-belief-revision.mjs} +7 -7
- package/{test-candidates.mjs → test/test-candidates.mjs} +4 -4
- package/{test-capture-diagnostics.mjs → test/test-capture-diagnostics.mjs} +6 -6
- package/{test-capture-field-lineage.mjs → test/test-capture-field-lineage.mjs} +4 -4
- package/{test-capture-inputs.mjs → test/test-capture-inputs.mjs} +8 -8
- package/{test-capture-jobs.mjs → test/test-capture-jobs.mjs} +5 -5
- package/{test-capture-projections.mjs → test/test-capture-projections.mjs} +12 -12
- package/{test-capture-revision.mjs → test/test-capture-revision.mjs} +6 -6
- package/{test-capture-submission.mjs → test/test-capture-submission.mjs} +3 -3
- package/{test-capture-validation-flow.mjs → test/test-capture-validation-flow.mjs} +5 -5
- package/{test-capture-validation-job.mjs → test/test-capture-validation-job.mjs} +5 -5
- package/{test-capture-validation-lineage.mjs → test/test-capture-validation-lineage.mjs} +2 -2
- package/{test-capture-validation.mjs → test/test-capture-validation.mjs} +2 -2
- package/{test-capture.mjs → test/test-capture.mjs} +20 -20
- package/{test-compaction-compat.mjs → test/test-compaction-compat.mjs} +5 -5
- package/{test-compat.mjs → test/test-compat.mjs} +3 -3
- package/{test-concurrency.mjs → test/test-concurrency.mjs} +3 -3
- package/{test-conflict.mjs → test/test-conflict.mjs} +2 -2
- package/{test-context-history.mjs → test/test-context-history.mjs} +1 -1
- package/{test-control-schema.mjs → test/test-control-schema.mjs} +1 -1
- package/test/test-detect.mjs +125 -0
- package/test/test-dream-plan.mjs +72 -0
- package/test/test-dsh-registration.mjs +111 -0
- package/{test-episodes.mjs → test/test-episodes.mjs} +5 -5
- package/{test-explicit-memory.mjs → test/test-explicit-memory.mjs} +4 -4
- package/{test-field-lineage.mjs → test/test-field-lineage.mjs} +4 -4
- package/{test-history.mjs → test/test-history.mjs} +4 -4
- package/{test-inline-summary.mjs → test/test-inline-summary.mjs} +5 -5
- package/{test-legacy-source-identity.mjs → test/test-legacy-source-identity.mjs} +2 -2
- package/{test-links.mjs → test/test-links.mjs} +4 -4
- package/{test-managed-access.mjs → test/test-managed-access.mjs} +29 -5
- package/{test-managed-activation.mjs → test/test-managed-activation.mjs} +5 -5
- package/{test-meaning.mjs → test/test-meaning.mjs} +6 -6
- package/{test-native-service-lifecycle.mjs → test/test-native-service-lifecycle.mjs} +1 -1
- package/{test-native-write-validation.mjs → test/test-native-write-validation.mjs} +4 -4
- package/{test-packaging.mjs → test/test-packaging.mjs} +32 -5
- package/{test-paired-analysis.mjs → test/test-paired-analysis.mjs} +1 -1
- package/{test-patch.mjs → test/test-patch.mjs} +11 -11
- package/{test-plugin-contract.mjs → test/test-plugin-contract.mjs} +4 -4
- package/{test-privacy.mjs → test/test-privacy.mjs} +3 -3
- package/{test-project-stores.mjs → test/test-project-stores.mjs} +5 -5
- package/{test-publication.mjs → test/test-publication.mjs} +4 -4
- package/{test-restore.mjs → test/test-restore.mjs} +11 -11
- package/{test-retrieval.mjs → test/test-retrieval.mjs} +2 -2
- package/{test-revision-admission.mjs → test/test-revision-admission.mjs} +3 -3
- package/{test-rule-admission.mjs → test/test-rule-admission.mjs} +8 -8
- package/{test-runtime-startup.mjs → test/test-runtime-startup.mjs} +2 -2
- package/{test-schema.mjs → test/test-schema.mjs} +2 -2
- package/{test-security.mjs → test/test-security.mjs} +6 -6
- package/test/test-semantic-suite.mjs +20 -0
- package/{test-service-readiness.mjs → test/test-service-readiness.mjs} +1 -1
- package/{test-smell.mjs → test/test-smell.mjs} +4 -4
- package/{test-snapshots.mjs → test/test-snapshots.mjs} +3 -3
- package/{test-source-completeness.mjs → test/test-source-completeness.mjs} +6 -6
- package/{test-source-key.mjs → test/test-source-key.mjs} +2 -2
- package/{test-source-retirement.mjs → test/test-source-retirement.mjs} +4 -4
- package/{test-status-both.mjs → test/test-status-both.mjs} +12 -4
- package/{test-supersede.mjs → test/test-supersede.mjs} +3 -3
- package/test/test-telemetry.mjs +60 -0
- package/{test-text-admission.mjs → test/test-text-admission.mjs} +6 -6
- package/{test-tool-inputs.mjs → test/test-tool-inputs.mjs} +1 -1
- package/{test-tool-observations.mjs → test/test-tool-observations.mjs} +6 -6
- package/{test-tool-write-guard.mjs → test/test-tool-write-guard.mjs} +3 -3
- package/{test-tool-write-lifecycle.mjs → test/test-tool-write-lifecycle.mjs} +3 -3
- package/{test-usability.mjs → test/test-usability.mjs} +7 -7
- package/{test-validation-source-refs.mjs → test/test-validation-source-refs.mjs} +2 -2
- package/{test-validation-transition-context.mjs → test/test-validation-transition-context.mjs} +1 -1
- package/{test-withdrawal-archives.mjs → test/test-withdrawal-archives.mjs} +6 -6
- package/{test-withdrawal.mjs → test/test-withdrawal.mjs} +2 -2
- package/{test.mjs → test/test.mjs} +4 -4
- package/version.mjs +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { setupMemoryHistory } from '../context-history.mjs'
|
|
7
|
+
import { apply } from '../index.mjs'
|
|
8
|
+
|
|
9
|
+
function deferredRegistration() {
|
|
10
|
+
let resolve, reject, calls = 0
|
|
11
|
+
const pending = new Promise((yes, no) => { resolve = yes; reject = no })
|
|
12
|
+
// Real DSH deliberately returns a callable thenable, not a native Promise.
|
|
13
|
+
const registration = Object.assign(() => { calls++ }, { then: pending.then.bind(pending) })
|
|
14
|
+
return { registration, resolve, reject, get calls() { return calls } }
|
|
15
|
+
}
|
|
16
|
+
const header = 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.'
|
|
17
|
+
function oldSession() {
|
|
18
|
+
const sections = [{ name: 'memory:index', text: 'old synthetic memory' }]
|
|
19
|
+
let messages = [{ id: 'old', role: 'user', source: { kind: 'runtime-context', sections }, content: [{ type: 'text', text: `${header}\n\nold synthetic memory` }] }]
|
|
20
|
+
return { id: 'synthetic-session', surface: { nodes: [1] }, deriveMessages: () => messages,
|
|
21
|
+
append(type, message, options) { assert.equal(type, 'user/message'); assert.deepEqual(options.sourceEventSeqs, [1]); messages = [message] } }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('callable asynchronous projection registration publishes no hooks before its retained handle is ready', async () => {
|
|
25
|
+
const deferred = deferredRegistration(), handlers = new Map(), session = oldSession()
|
|
26
|
+
let reads = 0
|
|
27
|
+
const ctx = { sessionProjections: { register: () => deferred.registration,
|
|
28
|
+
stateOf() { throw Error('deprecated registry reader must not be used') } },
|
|
29
|
+
workingDirectory: { get: () => '/synthetic' }, on: (name, fn) => handlers.set(name, fn) }
|
|
30
|
+
const pending = setupMemoryHistory(ctx, () => 'new synthetic memory')
|
|
31
|
+
assert.equal(handlers.size, 0)
|
|
32
|
+
deferred.resolve({ stateOf(value) { assert.equal(value, session); reads++; return { nodes: [{ id: 'old', seq: 1 }], overflow: false } } })
|
|
33
|
+
const history = await pending
|
|
34
|
+
assert.equal(history.available, true)
|
|
35
|
+
assert.equal(deferred.calls, 0, 'await must not invoke the legacy disposer')
|
|
36
|
+
assert.equal(handlers.get('agent/request')({ agent: { session } }, () => 'continued'), 'continued')
|
|
37
|
+
assert.equal(reads, 1)
|
|
38
|
+
assert.doesNotMatch(JSON.stringify(session.deriveMessages()), /old synthetic memory/)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('handle-only hosts need no deprecated registry stateOf method', async () => {
|
|
42
|
+
const handlers = new Map()
|
|
43
|
+
const history = await setupMemoryHistory({ sessionProjections: { register: async () => ({ stateOf: () => ({ nodes: [], overflow: false }) }) }, on: (name, fn) => handlers.set(name, fn) }, () => '')
|
|
44
|
+
assert.equal(history.available, true)
|
|
45
|
+
assert.equal(handlers.size, 4)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('rejected projection registration installs no history consumers', async () => {
|
|
49
|
+
const deferred = deferredRegistration(), handlers = []
|
|
50
|
+
const pending = setupMemoryHistory({ sessionProjections: { register: () => deferred.registration }, on: (...args) => handlers.push(args) }, () => '')
|
|
51
|
+
deferred.reject(Error('synthetic-registration-rejected'))
|
|
52
|
+
await assert.rejects(pending, /synthetic-registration-rejected/)
|
|
53
|
+
assert.deepEqual(handlers, [])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('malformed ready handle is rejected rather than advertised as available', async () => {
|
|
57
|
+
await assert.rejects(setupMemoryHistory({ sessionProjections: { register: async () => ({}) } }, () => ''), /memory-context-reader-unavailable/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
for (const success of [true, false]) test(`apply awaits registration before tools, prompts and success marker (${success ? 'success' : 'failure'})`, async () => {
|
|
61
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-registration-')), previous = process.env.DSH_HOME
|
|
62
|
+
process.env.DSH_HOME = root
|
|
63
|
+
const deferred = deferredRegistration(), tools = [], prompts = [], effects = [], handlers = []
|
|
64
|
+
const ctx = { sessionProjections: { register: () => deferred.registration },
|
|
65
|
+
systemPrompt: { context: value => prompts.push(value) }, workingDirectory: { get: () => root }, tools: { register: value => tools.push(value) },
|
|
66
|
+
on: (...args) => handlers.push(args), effect(fn) { effects.push(fn()) } }
|
|
67
|
+
try {
|
|
68
|
+
const pending = apply(ctx, { autoCapture: { enabled: false } })
|
|
69
|
+
assert.equal(typeof pending?.then, 'function')
|
|
70
|
+
assert.deepEqual(tools, []); assert.deepEqual(prompts, []); assert.deepEqual(handlers, [])
|
|
71
|
+
const marker = join(root, 'memory', '.loaded.json')
|
|
72
|
+
assert.equal(existsSync(marker), false)
|
|
73
|
+
if (success) {
|
|
74
|
+
deferred.resolve({ stateOf: () => ({ nodes: [], overflow: false }) })
|
|
75
|
+
await pending
|
|
76
|
+
assert.equal(tools.length, 6); assert.equal(prompts.length, 2)
|
|
77
|
+
assert.equal(JSON.parse(readFileSync(marker)).memoryHistory.available, true)
|
|
78
|
+
} else {
|
|
79
|
+
deferred.reject(Error('synthetic-owner-disposed'))
|
|
80
|
+
await assert.rejects(pending, /synthetic-owner-disposed/)
|
|
81
|
+
assert.equal(existsSync(marker), false); assert.deepEqual(tools, []); assert.deepEqual(prompts, []); assert.deepEqual(handlers, [])
|
|
82
|
+
}
|
|
83
|
+
assert.equal(deferred.calls, 0)
|
|
84
|
+
} finally {
|
|
85
|
+
for (const dispose of effects.reverse()) dispose?.()
|
|
86
|
+
if (previous === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = previous
|
|
87
|
+
rmSync(root, { recursive: true, force: true })
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
test('a successful handle arriving after owner disposal cannot activate history or tools', async () => {
|
|
93
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-disposed-registration-')), previous = process.env.DSH_HOME
|
|
94
|
+
process.env.DSH_HOME = root
|
|
95
|
+
const deferred = deferredRegistration(), consumers = [], fiber = { uid: 1 }
|
|
96
|
+
const ctx = { fiber, sessionProjections: { register: () => deferred.registration },
|
|
97
|
+
systemPrompt: { context: value => consumers.push(value) }, workingDirectory: { get: () => root },
|
|
98
|
+
tools: { register: value => consumers.push(value) }, on: (...args) => consumers.push(args) }
|
|
99
|
+
try {
|
|
100
|
+
const pending = apply(ctx, { autoCapture: { enabled: false } })
|
|
101
|
+
fiber.uid = null
|
|
102
|
+
deferred.resolve({ stateOf: () => ({ nodes: [], overflow: false }) })
|
|
103
|
+
await pending
|
|
104
|
+
assert.deepEqual(consumers, [])
|
|
105
|
+
assert.equal(existsSync(join(root, 'memory', '.loaded.json')), false)
|
|
106
|
+
assert.equal(deferred.calls, 0)
|
|
107
|
+
} finally {
|
|
108
|
+
if (previous === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = previous
|
|
109
|
+
rmSync(root, { recursive: true, force: true })
|
|
110
|
+
}
|
|
111
|
+
})
|
|
@@ -4,11 +4,11 @@ import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync,exi
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {apply,indexLine,searchStore,forgetMemory,forgetExpired,pruneStore,withdrawManagedSource,recoverMemoryPublication,adoptCandidate,storeRevision,storeDirFor} from '
|
|
8
|
-
import {candidateSnapshot,stageCandidate} from '
|
|
9
|
-
import {writeFixtureMemory} from '
|
|
10
|
-
import {episodeSnapshot,prepareEpisodes,historyRows,EPISODE_FILE,EPISODE_LIMIT} from '
|
|
11
|
-
import {toolProposal} from '
|
|
7
|
+
import {apply,indexLine,searchStore,forgetMemory,forgetExpired,pruneStore,withdrawManagedSource,recoverMemoryPublication,adoptCandidate,storeRevision,storeDirFor} from '../index.mjs'
|
|
8
|
+
import {candidateSnapshot,stageCandidate} from '../candidates.mjs'
|
|
9
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
10
|
+
import {episodeSnapshot,prepareEpisodes,historyRows,EPISODE_FILE,EPISODE_LIMIT} from '../episodes.mjs'
|
|
11
|
+
import {toolProposal} from '../source-record.mjs'
|
|
12
12
|
|
|
13
13
|
const digest=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex')
|
|
14
14
|
async function box(run){
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {memoryProtocol} from '
|
|
4
|
-
import {captureSourceAliases} from '
|
|
5
|
-
import {extractWithLlm} from '
|
|
6
|
-
import {assessCandidate} from '
|
|
3
|
+
import {memoryProtocol} from '../memory-protocol.mjs'
|
|
4
|
+
import {captureSourceAliases} from '../capture-source-aliases.mjs'
|
|
5
|
+
import {extractWithLlm} from '../capture-llm.mjs'
|
|
6
|
+
import {assessCandidate} from '../capture-evidence.mjs'
|
|
7
7
|
|
|
8
8
|
test('protocol reports scoped availability and remains available with no memory data', () => {
|
|
9
9
|
const agent={session:{id:'owner'}}
|
|
@@ -5,10 +5,10 @@ import {createHash} from 'node:crypto'
|
|
|
5
5
|
import {mkdtempSync,mkdirSync,rmSync,readFileSync,writeFileSync} from 'node:fs'
|
|
6
6
|
import {join} from 'node:path'
|
|
7
7
|
import {tmpdir} from 'node:os'
|
|
8
|
-
import {compileValueRevision,normalizeFieldLineage} from '
|
|
9
|
-
import {writeMemory,indexLine,searchStore,withdrawManagedSource,forgetMemory,recoverMemoryPublication,storeRevision} from '
|
|
10
|
-
import {candidateSnapshot,stageCandidate,rejectCandidate,pruneCandidates} from '
|
|
11
|
-
import {episodeSnapshot} from '
|
|
8
|
+
import {compileValueRevision,normalizeFieldLineage} from '../field-lineage.mjs'
|
|
9
|
+
import {writeMemory,indexLine,searchStore,withdrawManagedSource,forgetMemory,recoverMemoryPublication,storeRevision} from '../index.mjs'
|
|
10
|
+
import {candidateSnapshot,stageCandidate,rejectCandidate,pruneCandidates} from '../candidates.mjs'
|
|
11
|
+
import {episodeSnapshot} from '../episodes.mjs'
|
|
12
12
|
const hash=v=>createHash('sha256').update(JSON.stringify(v)).digest('hex')
|
|
13
13
|
const meaning={kind:'preference',conditions:[{dimension:'document_kind',value:'draft'}],except:[{dimension:'audience',value:'experts'}]}
|
|
14
14
|
const proof=n=>({sessionId:'owner-session-'+n,basis:'tool-context',via:'memory_write',callHash:hash(['call',n]),evidence:[{messageId:'message-'+n,hash:hash(['message',n]),quote:'Synthetic source '+n+' for lasting draft palette.'}]})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {realpathSync} from 'node:fs'
|
|
2
|
-
import {applyWithOwnerFixture as apply} from '
|
|
2
|
+
import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
|
|
3
3
|
/**
|
|
4
|
-
* 历史链与写入相似度闸。跑法:node --test ~/.dsh/plugins/memory/test-history.mjs
|
|
4
|
+
* 历史链与写入相似度闸。跑法:node --test ~/.dsh/plugins/memory/test/test-history.mjs
|
|
5
5
|
*
|
|
6
6
|
* 两件事各自的「为什么」:
|
|
7
7
|
* · 历史链:审计回答「做过什么」,历史回答「原来是什么」。取代/删除后原文必须可回溯,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
storeDirFor,
|
|
25
25
|
summaryOf,
|
|
26
26
|
writeMemory,
|
|
27
|
-
} from '
|
|
27
|
+
} from '../index.mjs'
|
|
28
28
|
|
|
29
29
|
function sandbox() {
|
|
30
30
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-hist-')))
|
|
@@ -272,7 +272,7 @@ test('工具调用遇到近似重复时,错误信息足以让模型自行修
|
|
|
272
272
|
// ── 审计发现的两个数据安全问题(回归) ──────────────────────────────────────
|
|
273
273
|
|
|
274
274
|
test('主题文件超出读取上限时拒绝追加,绝不覆盖历史', async () => {
|
|
275
|
-
const { appendTopic } = await import('
|
|
275
|
+
const { appendTopic } = await import('../index.mjs')
|
|
276
276
|
const box = sandbox()
|
|
277
277
|
try {
|
|
278
278
|
const dir = join(box.cwd, 'store')
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {realpathSync} from 'node:fs'
|
|
2
|
-
import {applyWithOwnerFixture as apply} from '
|
|
2
|
+
import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
|
|
3
3
|
import {test} from 'node:test'
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
5
5
|
import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,existsSync} from 'node:fs'
|
|
6
6
|
import {join} from 'node:path'
|
|
7
7
|
import {tmpdir} from 'node:os'
|
|
8
|
-
import {writeMemory,storeDirFor,indexLine,summaryOf,isExpired,lookupLinks,storeRevision,adoptCandidate,withdrawManagedSource,searchStore} from '
|
|
9
|
-
import {stageCandidate} from '
|
|
10
|
-
import {entryId} from '
|
|
11
|
-
const legacy=JSON.parse(readFileSync(new URL('
|
|
8
|
+
import {writeMemory,storeDirFor,indexLine,summaryOf,isExpired,lookupLinks,storeRevision,adoptCandidate,withdrawManagedSource,searchStore} from '../index.mjs'
|
|
9
|
+
import {stageCandidate} from '../candidates.mjs'
|
|
10
|
+
import {entryId} from '../security.mjs'
|
|
11
|
+
const legacy=JSON.parse(readFileSync(new URL('../corpus/legacy-summary-v15.json',import.meta.url)))
|
|
12
12
|
function box(scope='global'){const root=realpathSync(mkdtempSync(join(tmpdir(),'dsh-inline-summary-'))),home=join(root,'home');mkdirSync(home,{recursive:true});const previous=process.env.DSH_HOME;process.env.DSH_HOME=home;const dir=storeDirFor(scope,root);mkdirSync(dir,{recursive:true});let mounted
|
|
13
13
|
function mount(){if(mounted)return mounted;const tools=[],captured={};apply({systemPrompt:{context:provider=>captured.provider=provider},tools:{register:tool=>tools.push(tool)},workingDirectory:{get:()=>root},on(){}},{autoCapture:{enabled:false}});return mounted={tools,provider:captured.provider}}
|
|
14
14
|
return{root,dir,get tools(){return mount().tools},get provider(){return mount().provider},close(){if(previous===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=previous;rmSync(root,{recursive:true,force:true})}}
|
|
@@ -4,8 +4,8 @@ import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,realpathSync} fr
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {initializeManagedMemory,managedMemoryStatus,writeMemory,indexLine,withdrawManagedSource,withdrawManagedSourceKey,searchStore} from '
|
|
8
|
-
import {withdrawalSelector,discoverWithdrawalIdentity} from '
|
|
7
|
+
import {initializeManagedMemory,managedMemoryStatus,writeMemory,indexLine,withdrawManagedSource,withdrawManagedSourceKey,searchStore} from '../index.mjs'
|
|
8
|
+
import {withdrawalSelector,discoverWithdrawalIdentity} from '../source-identity.mjs'
|
|
9
9
|
const hash=value=>createHash('sha256').update(value).digest('hex'),key=(sid,msg)=>hash(JSON.stringify([sid,msg]))
|
|
10
10
|
const sid='synthetic-current-owner',msg='synthetic-current-message',target=key(sid,msg)
|
|
11
11
|
function box(run,{legacySid='synthetic-different-legacy-owner',auditOnly=false}={}){
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {realpathSync} from 'node:fs'
|
|
2
|
-
import {applyWithOwnerFixture as apply} from '
|
|
2
|
+
import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
|
|
3
3
|
/**
|
|
4
|
-
* 条目 id、关系边与多跳查询。跑法:node --test ~/.dsh/plugins/memory/test-links.mjs
|
|
4
|
+
* 条目 id、关系边与多跳查询。跑法:node --test ~/.dsh/plugins/memory/test/test-links.mjs
|
|
5
5
|
*
|
|
6
6
|
* 对应两项差距:MemOS 的「版本可查询」与 A-MEM 的「插入时建链」。
|
|
7
7
|
* id 用内容寻址(sha1 前 8 位),不新增字段、不改索引格式:一行内容的 id 就是它自己。
|
|
@@ -13,8 +13,8 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, existsSync } from 'node:f
|
|
|
13
13
|
import { join } from 'node:path'
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
|
|
16
|
-
import { entryId } from '
|
|
17
|
-
import { LINK_LIMIT, LINK_THRESHOLD, lookupLinks, relatedEntries, storeDirFor, writeMemory } from '
|
|
16
|
+
import { entryId } from '../security.mjs'
|
|
17
|
+
import { LINK_LIMIT, LINK_THRESHOLD, lookupLinks, relatedEntries, storeDirFor, writeMemory } from '../index.mjs'
|
|
18
18
|
|
|
19
19
|
function sandbox() {
|
|
20
20
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-links-')))
|
|
@@ -4,11 +4,13 @@ import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,cpSync,readdirSy
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import {once} from 'node:events'
|
|
8
|
+
import {Worker} from 'node:worker_threads'
|
|
9
|
+
import {writeMemory,indexLine,searchStore,lookupLinks,statusOf,storeRevision,withdrawManagedSource,recoverMemoryPublication,initializeManagedMemory,managedMemoryStatus,adoptCandidate,apply,storeDirFor} from '../index.mjs'
|
|
10
|
+
import {candidateSnapshot,listCandidates,stageCandidate,withdrawSource,withdrawalPolicy} from '../candidates.mjs'
|
|
11
|
+
import {legacyProjectSlug,inspectProjectStore,migrateProjectStore,projectStoreDir} from '../project-stores.mjs'
|
|
12
|
+
import {managedRead,managedProjectOperation} from '../managed-access.mjs'
|
|
13
|
+
import {withManagedRead} from '../managed-policy.mjs'
|
|
12
14
|
const hash=value=>createHash('sha256').update(value).digest('hex')
|
|
13
15
|
const proof=(fresh=false)=>({basis:'tool-context',via:'memory_write',sessionId:fresh?'unrelated-owner':'managed-owner',callHash:hash(fresh?'other-call':'managed-call'),evidence:[{messageId:fresh?'unrelated-root':'managed-root',hash:hash('synthetic-source'),quote:fresh?'Synthetic unrelated delivery choice cobalt7654':'Synthetic palette choice jade9876'}]})
|
|
14
16
|
const box=run=>{const root=realpathSync(mkdtempSync(join(tmpdir(),'managed-access-'))),data=join(root,'memory');mkdirSync(data);try{const result=run({root,data,rows:[{name:'.',role:'global'},{name:'projects/v2-synthetic',role:'project'}],project:join(data,'projects/v2-synthetic')});if(result&&typeof result.then==='function')return result.finally(()=>rmSync(root,{recursive:true,force:true}));rmSync(root,{recursive:true,force:true});return result}catch(error){rmSync(root,{recursive:true,force:true});throw error}}
|
|
@@ -20,6 +22,28 @@ test('a read never creates a missing control database or invokes the data callba
|
|
|
20
22
|
let read=false;assert.throws(()=>withManagedRead(f.data,'.',()=>{read=true}),/control-missing/)
|
|
21
23
|
assert.equal(read,false);assert.equal(existsSync(f.data+'.control/state.sqlite'),false)
|
|
22
24
|
}))
|
|
25
|
+
test('a managed read recovers after a brief control database lock',()=>box(async f=>{
|
|
26
|
+
seeded(f);initialize(f)
|
|
27
|
+
const worker=new Worker(`
|
|
28
|
+
const {DatabaseSync}=require('node:sqlite')
|
|
29
|
+
const {parentPort,workerData}=require('node:worker_threads')
|
|
30
|
+
const db=new DatabaseSync(workerData)
|
|
31
|
+
try{
|
|
32
|
+
db.exec('BEGIN IMMEDIATE')
|
|
33
|
+
parentPort.postMessage('locked')
|
|
34
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,220)
|
|
35
|
+
db.exec('ROLLBACK')
|
|
36
|
+
}finally{db.close()}
|
|
37
|
+
`,{eval:true,workerData:f.data+'.control/state.sqlite'})
|
|
38
|
+
const finished=once(worker,'exit')
|
|
39
|
+
try{
|
|
40
|
+
await once(worker,'message')
|
|
41
|
+
assert.ok(searchStore(f.data,'jade9876').hits.length>0)
|
|
42
|
+
}finally{
|
|
43
|
+
const [code]=await finished
|
|
44
|
+
assert.equal(code,0)
|
|
45
|
+
}
|
|
46
|
+
}))
|
|
23
47
|
test('transaction identity rejection happens before fresh-source and data callbacks',()=>box(f=>{
|
|
24
48
|
seeded(f);initialize(f);let fresh=false,read=false
|
|
25
49
|
assert.throws(()=>withManagedRead(f.data,'.',()=>{read=true},{validateIdentity(){throw Error('synthetic-identity-rejected')},readFreshProofs(){fresh=true;return[]}}),/identity-rejected/)
|
|
@@ -6,11 +6,11 @@ import {tmpdir} from 'node:os'
|
|
|
6
6
|
import {fileURLToPath} from 'node:url'
|
|
7
7
|
import {createHash} from 'node:crypto'
|
|
8
8
|
import {spawnSync} from 'node:child_process'
|
|
9
|
-
import {writeMemory,indexLine,withdrawManagedSource,searchStore,managedMemoryStatus,initializeManagedMemory,storeRevision,adoptCandidate} from '
|
|
10
|
-
import {stageCandidate} from '
|
|
11
|
-
import {activateManagedMemory,activationStores} from '
|
|
12
|
-
import {probeEnvironment} from '
|
|
13
|
-
const here=dirname(fileURLToPath(import.meta.url)),hash=value=>createHash('sha256').update(value).digest('hex')
|
|
9
|
+
import {writeMemory,indexLine,withdrawManagedSource,searchStore,managedMemoryStatus,initializeManagedMemory,storeRevision,adoptCandidate} from '../index.mjs'
|
|
10
|
+
import {stageCandidate} from '../candidates.mjs'
|
|
11
|
+
import {activateManagedMemory,activationStores} from '../managed-activation.mjs'
|
|
12
|
+
import {probeEnvironment} from '../scripts/probe-env.mjs'
|
|
13
|
+
const here=dirname(dirname(fileURLToPath(import.meta.url))),hash=value=>createHash('sha256').update(value).digest('hex')
|
|
14
14
|
function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'managed-activation-'))),data=join(root,'memory');mkdirSync(data);try{return run({root,data})}finally{rmSync(root,{recursive:true,force:true})}}
|
|
15
15
|
function write(dir,scope='global',owner='old-owner',word='jade9876'){
|
|
16
16
|
const summary='Synthetic activation preference '+word
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {applyWithValidationFixture as apply} from '
|
|
1
|
+
import {applyWithValidationFixture as apply} from '../fixture-host.mjs'
|
|
2
2
|
import {test} from 'node:test'
|
|
3
3
|
import assert from 'node:assert/strict'
|
|
4
4
|
import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,existsSync,rmSync} from 'node:fs'
|
|
5
5
|
import {join} from 'node:path'
|
|
6
6
|
import {tmpdir} from 'node:os'
|
|
7
7
|
import {createHash} from 'node:crypto'
|
|
8
|
-
import {normalizeMeaning,meaningForScope,normalizeContext,normalizeTask,applicability,meaningView,memoryAdvice,detailedMeanings,meaningLabel} from '
|
|
9
|
-
import {indexLine, searchStore, writeMemory, storeRevision, withdrawManagedSource, adoptCandidate, storeDirFor} from '
|
|
10
|
-
import {candidateSnapshot,stageCandidate} from '
|
|
11
|
-
import {writeFixtureMemory,applyWithOwnerFixture} from '
|
|
12
|
-
import {captureMessage,assessCandidate} from '
|
|
8
|
+
import {normalizeMeaning,meaningForScope,normalizeContext,normalizeTask,applicability,meaningView,memoryAdvice,detailedMeanings,meaningLabel} from '../meaning.mjs'
|
|
9
|
+
import {indexLine, searchStore, writeMemory, storeRevision, withdrawManagedSource, adoptCandidate, storeDirFor} from '../index.mjs'
|
|
10
|
+
import {candidateSnapshot,stageCandidate} from '../candidates.mjs'
|
|
11
|
+
import {writeFixtureMemory,applyWithOwnerFixture} from '../fixture-host.mjs'
|
|
12
|
+
import {captureMessage,assessCandidate} from '../capture-evidence.mjs'
|
|
13
13
|
|
|
14
14
|
const condition={dimension:'audience',value:'children'},exception={dimension:'delivery',value:'urgent'}
|
|
15
15
|
const pref=normalizeMeaning({kind:'preference',conditions:[condition]})
|
|
@@ -5,7 +5,7 @@ import {mkdtempSync,realpathSync,mkdirSync,rmSync} from 'node:fs'
|
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {join,resolve} from 'node:path'
|
|
7
7
|
import {pathToFileURL} from 'node:url'
|
|
8
|
-
const source=process.env.DSH_NATIVE_SERVICE_SOURCE?pathToFileURL(resolve(process.env.DSH_NATIVE_SERVICE_SOURCE)+'/'):new URL('
|
|
8
|
+
const source=process.env.DSH_NATIVE_SERVICE_SOURCE?pathToFileURL(resolve(process.env.DSH_NATIVE_SERVICE_SOURCE)+'/'):new URL('../',import.meta.url)
|
|
9
9
|
const {apply}=await import(new URL('index.mjs',source)),{candidateSnapshot}=await import(new URL('candidates.mjs',source)),{scriptedNativeValidationLlm}=await import(new URL('fixture-host.mjs',source))
|
|
10
10
|
for(const mode of ['stable-owner-fresh-proxies','owner-disappears','owner-replaced'])test('native service lifecycle: '+mode,async()=>{
|
|
11
11
|
const base=realpathSync(mkdtempSync(join(tmpdir(),'native-service-generation-'))),home=join(base,'home'),old=process.env.DSH_HOME;mkdirSync(home);process.env.DSH_HOME=home
|
|
@@ -4,10 +4,10 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import {mkdtempSync,realpathSync,mkdirSync,rmSync,readFileSync,existsSync} from 'node:fs'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {join} from 'node:path'
|
|
7
|
-
import {apply,storeDirFor,searchStore,withdrawManagedSource} from '
|
|
8
|
-
import {candidateSnapshot} from '
|
|
9
|
-
import {VALIDATION_FIELDS} from '
|
|
10
|
-
import {writeFixtureMemory} from '
|
|
7
|
+
import {apply,storeDirFor,searchStore,withdrawManagedSource} from '../index.mjs'
|
|
8
|
+
import {candidateSnapshot} from '../candidates.mjs'
|
|
9
|
+
import {VALIDATION_FIELDS} from '../capture-validation-contract.mjs'
|
|
10
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
11
11
|
const proposal={type:'user',scope:'global',summary:'长期反馈先列两处问题再给办法',meaning:{kind:'preference',conditions:[{dimension:'task',value:'review'}]}}
|
|
12
12
|
const text='跨我的所有项目,长期反馈都先列两处问题再给修改办法。'
|
|
13
13
|
async function box(options,run){
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 打包完整性。跑法:node --test ~/.dsh/plugins/memory/test-packaging.mjs
|
|
2
|
+
* 打包完整性。跑法:node --test ~/.dsh/plugins/memory/test/test-packaging.mjs
|
|
3
3
|
*
|
|
4
4
|
* 发布前最容易漏的四件事:版本两处不一致、manifest 字段缺失、files 漏了运行时文件、
|
|
5
5
|
* 文档里指向不存在的文件。这些都是机械可查的,不该靠人记得。
|
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import { test } from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
|
-
import { existsSync, readFileSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs'
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs'
|
|
11
11
|
import { join, dirname } from 'node:path'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
import { tmpdir } from 'node:os'
|
|
14
14
|
import { spawnSync } from 'node:child_process'
|
|
15
|
-
import { probeEnvironment } from '
|
|
15
|
+
import { probeEnvironment } from '../scripts/probe-env.mjs'
|
|
16
16
|
|
|
17
|
-
import { VERSION } from '
|
|
17
|
+
import { VERSION } from '../version.mjs'
|
|
18
18
|
|
|
19
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const HERE = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
20
20
|
const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
|
|
21
21
|
|
|
22
22
|
test('版本单点:package.json 与 version.mjs 一致', () => {
|
|
@@ -72,6 +72,15 @@ test('npm scripts 指向真实存在的测试文件', () => {
|
|
|
72
72
|
for (const file of files) assert.ok(existsSync(join(HERE, file)), `脚本引用了不存在的 ${file}`)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
test('测试集中在 test/,命令与发布清单完整覆盖同一集合', () => {
|
|
76
|
+
const isSuite = name => /^test(?:-[a-z0-9-]+)?\.mjs$/.test(name)
|
|
77
|
+
assert.deepEqual(readdirSync(HERE).filter(isSuite), [], '根目录不应再有测试入口')
|
|
78
|
+
const suites = readdirSync(join(HERE, 'test')).filter(isSuite).map(name => `test/${name}`).sort()
|
|
79
|
+
const declared = pkg.scripts.test.split(/\s+/).filter(token => token.endsWith('.mjs')).sort()
|
|
80
|
+
assert.deepEqual(declared, suites, 'npm test 应覆盖全部测试且没有重复项')
|
|
81
|
+
for (const suite of suites) assert.ok(pkg.files.includes(suite), `files 漏了 ${suite}`)
|
|
82
|
+
})
|
|
83
|
+
|
|
75
84
|
test('LICENSE 与 package.json 的 license 字段一致', () => {
|
|
76
85
|
assert.equal(pkg.license, 'MIT')
|
|
77
86
|
assert.match(readFileSync(join(HERE, 'LICENSE'), 'utf8'), /MIT License/)
|
|
@@ -100,3 +109,21 @@ test('真实探针子进程看不到合成凭据或 NODE_OPTIONS,home/tmp 均
|
|
|
100
109
|
assert.doesNotMatch(child.stdout,/synthetic-key/)
|
|
101
110
|
} finally {rmSync(root,{recursive:true,force:true})}
|
|
102
111
|
})
|
|
112
|
+
|
|
113
|
+
// This executes from both the source tree and the extracted npm archive.
|
|
114
|
+
test('随包示例执行真实处理器,保留来源并隔离不同项目(非真实模型验收)', () => {
|
|
115
|
+
const child = spawnSync(process.execPath, [join(HERE, 'scripts/demo-source-recall.mjs'), '--json'], { encoding: 'utf8', timeout: 30000 })
|
|
116
|
+
assert.equal(child.status, 0, child.stderr)
|
|
117
|
+
const receipt = JSON.parse(child.stdout)
|
|
118
|
+
assert.equal(receipt.kind, 'synthetic-scripted-host-mechanics')
|
|
119
|
+
assert.equal(receipt.realDshHost, false)
|
|
120
|
+
assert.equal(receipt.realModel, false)
|
|
121
|
+
assert.equal(receipt.networkRequests, 0)
|
|
122
|
+
assert.equal(receipt.isolatedTemporaryHome, true)
|
|
123
|
+
assert.equal(receipt.separateProcesses, 3)
|
|
124
|
+
assert.equal(receipt.phases.write.result.status, 'recorded')
|
|
125
|
+
assert.equal(receipt.phases.recall.current.hits.length, 1)
|
|
126
|
+
assert.equal(receipt.phases.recall.history.hits.length, 1)
|
|
127
|
+
assert.equal(receipt.phases['other-project'].current.hits.length, 0)
|
|
128
|
+
assert.equal(receipt.phases['other-project'].history.hits.length, 0)
|
|
129
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import { analyzePairs, signTail } from '
|
|
3
|
+
import { analyzePairs, signTail } from '../eval/paired-analysis.mjs'
|
|
4
4
|
|
|
5
5
|
test('配对精确检验:已知二项尾概率', () => {
|
|
6
6
|
assert.equal(signTail(0, 0), 1)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 安装/卸载脚本。跑法:node --test ~/.dsh/plugins/memory/test-patch.mjs
|
|
2
|
+
* 安装/卸载脚本。跑法:node --test ~/.dsh/plugins/memory/test/test-patch.mjs
|
|
3
3
|
*
|
|
4
4
|
* 这类脚本最容易在"用户的文件"上出事:删多了、重复插、覆盖掉别人的配置。
|
|
5
5
|
* 所以测试重点不是"能不能装上",而是"只动自己那几行"。
|
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os'
|
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
14
14
|
import { spawnSync } from 'node:child_process'
|
|
15
15
|
|
|
16
|
-
import { run } from '
|
|
16
|
+
import { run } from '../scripts/patch.mjs'
|
|
17
17
|
|
|
18
18
|
const OTHER = `# 用户自己的配置
|
|
19
19
|
- id: locale
|
|
@@ -57,7 +57,7 @@ test('插件路径不存在时拒绝安装,且不改动文件', () => {
|
|
|
57
57
|
test('安装:真实路径下写入块,且原有条目一行不少', () => {
|
|
58
58
|
const box = sandbox()
|
|
59
59
|
try {
|
|
60
|
-
const plugin = fileURLToPath(new URL('
|
|
60
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
61
61
|
const result = run(['install', '--profile', box.profile, '--plugin', plugin])
|
|
62
62
|
assert.equal(result.changed, true)
|
|
63
63
|
|
|
@@ -79,7 +79,7 @@ test('安装:真实路径下写入块,且原有条目一行不少', () => {
|
|
|
79
79
|
test('幂等:装两次不产生第二个块', () => {
|
|
80
80
|
const box = sandbox()
|
|
81
81
|
try {
|
|
82
|
-
const plugin = fileURLToPath(new URL('
|
|
82
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
83
83
|
run(['install', '--profile', box.profile, '--plugin', plugin])
|
|
84
84
|
const first = readFileSync(box.file, 'utf8')
|
|
85
85
|
|
|
@@ -95,7 +95,7 @@ test('幂等:装两次不产生第二个块', () => {
|
|
|
95
95
|
test('卸载:只删自己的块,用户配置逐字保留', () => {
|
|
96
96
|
const box = sandbox()
|
|
97
97
|
try {
|
|
98
|
-
const plugin = fileURLToPath(new URL('
|
|
98
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
99
99
|
run(['install', '--profile', box.profile, '--plugin', plugin])
|
|
100
100
|
const result = run(['uninstall', '--profile', box.profile])
|
|
101
101
|
assert.equal(result.changed, true)
|
|
@@ -124,7 +124,7 @@ test('卸载:没装过时不动文件', () => {
|
|
|
124
124
|
test('dry-run 不落盘', () => {
|
|
125
125
|
const box = sandbox()
|
|
126
126
|
try {
|
|
127
|
-
const plugin = fileURLToPath(new URL('
|
|
127
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
128
128
|
const before = readFileSync(box.file, 'utf8')
|
|
129
129
|
const result = run(['install', '--profile', box.profile, '--plugin', plugin, '--dry-run'])
|
|
130
130
|
assert.equal(result.dryRun, true)
|
|
@@ -138,7 +138,7 @@ test('dry-run 不落盘', () => {
|
|
|
138
138
|
test('标记不成对时拒绝操作(不猜)', () => {
|
|
139
139
|
const box = sandbox(`${OTHER}# >>> dsh-memory\n- insert:\n`)
|
|
140
140
|
try {
|
|
141
|
-
assert.throws(() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
141
|
+
assert.throws(() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))]), /标记不成对/)
|
|
142
142
|
assert.throws(() => run(['uninstall', '--profile', box.profile]), /标记不成对/)
|
|
143
143
|
} finally {
|
|
144
144
|
box.cleanup()
|
|
@@ -158,7 +158,7 @@ test('已有未托管的 memory 条目时拒绝安装(不制造副本)', ()
|
|
|
158
158
|
try {
|
|
159
159
|
const before = readFileSync(box.file, 'utf8')
|
|
160
160
|
assert.throws(
|
|
161
|
-
() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
161
|
+
() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))]),
|
|
162
162
|
/未托管的 memory 条目/,
|
|
163
163
|
)
|
|
164
164
|
assert.equal(readFileSync(box.file, 'utf8'), before, '拒绝时不应改动文件')
|
|
@@ -171,7 +171,7 @@ test('全新空列表配置可安装,卸载逐字恢复注释与空白', () =>
|
|
|
171
171
|
const original = '# clean profile\n [] # intentional empty\n\n'
|
|
172
172
|
const box = sandbox(original)
|
|
173
173
|
try {
|
|
174
|
-
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
174
|
+
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))])
|
|
175
175
|
const installed = readFileSync(box.file, 'utf8')
|
|
176
176
|
assert.doesNotMatch(installed, /^\s*\[\]/m)
|
|
177
177
|
assert.match(installed, /^- insert:/m)
|
|
@@ -183,7 +183,7 @@ test('全新空列表配置可安装,卸载逐字恢复注释与空白', () =>
|
|
|
183
183
|
test('恢复空列表时保留托管块以外的新注释,损坏恢复标记则拒绝', () => {
|
|
184
184
|
const box = sandbox('[]\n')
|
|
185
185
|
try {
|
|
186
|
-
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
186
|
+
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))])
|
|
187
187
|
const installed = readFileSync(box.file, 'utf8')
|
|
188
188
|
writeFileSync(box.file, '# newer outside comment\n' + installed)
|
|
189
189
|
run(['uninstall', '--profile', box.profile])
|
|
@@ -212,7 +212,7 @@ test('插件路径中的空格、中文与井号使用有效 file URL', () => {
|
|
|
212
212
|
test('default plugin path installs correctly from a Unicode directory with spaces and URL metacharacters',()=>{
|
|
213
213
|
const box=sandbox()
|
|
214
214
|
try{
|
|
215
|
-
const local=join(box.root,'插件 空间#'),scripts=join(local,'scripts');mkdirSync(scripts,{recursive:true});writeFileSync(join(local,'index.mjs'),'export function apply() {}\n');const file=join(scripts,'patch.mjs');copyFileSync(fileURLToPath(new URL('
|
|
215
|
+
const local=join(box.root,'插件 空间#'),scripts=join(local,'scripts');mkdirSync(scripts,{recursive:true});writeFileSync(join(local,'index.mjs'),'export function apply() {}\n');const file=join(scripts,'patch.mjs');copyFileSync(fileURLToPath(new URL('../scripts/patch.mjs',import.meta.url)),file)
|
|
216
216
|
const result=spawnSync(process.execPath,[file,'install','--profile',box.profile],{encoding:'utf8'});assert.equal(result.status,0,result.stderr);const body=readFileSync(box.file,'utf8'),href=JSON.parse(body.split('\n').filter(line=>line.trim().startsWith('name:')).at(-1).split('name:')[1].trim());assert.equal(realpathSync(fileURLToPath(href)),realpathSync(join(local,'index.mjs')));assert.ok(href.includes('%20')&&href.includes('%23'));assert.ok(body.includes('id: mcp-godot'))
|
|
217
217
|
}finally{box.cleanup()}
|
|
218
218
|
})
|
|
@@ -4,10 +4,10 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import { mkdtempSync, mkdirSync, readFileSync, existsSync, rmSync } from 'node:fs'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
-
import { Config, normalizePluginConfig, DEFAULT_AUTO_CAPTURE } from '
|
|
8
|
-
import { optionalService } from '
|
|
9
|
-
import { extractWithLlm } from '
|
|
10
|
-
import { apply,storeDirFor } from '
|
|
7
|
+
import { Config, normalizePluginConfig, DEFAULT_AUTO_CAPTURE } from '../plugin-config.mjs'
|
|
8
|
+
import { optionalService } from '../host-services.mjs'
|
|
9
|
+
import { extractWithLlm } from '../capture-llm.mjs'
|
|
10
|
+
import { apply,storeDirFor } from '../index.mjs'
|
|
11
11
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
12
12
|
async function until(check) { for (let i = 0; i < 200; i++) { if (check()) return; await sleep(5) } throw Error('fixture deadline') }
|
|
13
13
|
|
|
@@ -3,9 +3,9 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync} from 'node:fs'
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
|
-
import {apply,searchStore,lookupLinks} from '
|
|
7
|
-
import {writeFixtureMemory} from '
|
|
8
|
-
import {memoryPrivacy,privateMemory,publicMemoryRecords,publicMemoryIndex} from '
|
|
6
|
+
import {apply,searchStore,lookupLinks} from '../index.mjs'
|
|
7
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
8
|
+
import {memoryPrivacy,privateMemory,publicMemoryRecords,publicMemoryIndex} from '../privacy.mjs'
|
|
9
9
|
|
|
10
10
|
test('actual background extractor receives no unrelated saved private source or summary',async()=>{
|
|
11
11
|
const home=realpathSync(mkdtempSync(join(tmpdir(),'memory-private-background-'))),old=process.env.DSH_HOME;process.env.DSH_HOME=home
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {prepareMountedFixture} from '
|
|
1
|
+
import {prepareMountedFixture} from '../fixture-host.mjs'
|
|
2
2
|
import {realpathSync} from 'node:fs'
|
|
3
3
|
import { test } from 'node:test'
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
@@ -7,8 +7,8 @@ import { spawnSync } from 'node:child_process'
|
|
|
7
7
|
import { join } from 'node:path'
|
|
8
8
|
import { tmpdir } from 'node:os'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
|
-
import { projectStoreDir, projectIdentity, legacyProjectSlug, inspectProjectStore, migrateProjectStore, recoverProjectStore } from '
|
|
11
|
-
import { storeDirFor, writeMemory, indexLine, searchStore, apply } from '
|
|
10
|
+
import { projectStoreDir, projectIdentity, legacyProjectSlug, inspectProjectStore, migrateProjectStore, recoverProjectStore } from '../project-stores.mjs'
|
|
11
|
+
import { storeDirFor, writeMemory, indexLine, searchStore, apply } from '../index.mjs'
|
|
12
12
|
|
|
13
13
|
function box(run) {
|
|
14
14
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-project-store-'))), store = join(root, 'memory'), a = join(root, 'A B'), b = join(root, 'A-B')
|
|
@@ -160,7 +160,7 @@ test('a live loaded host blocks applying migration while read-only inspection re
|
|
|
160
160
|
}))
|
|
161
161
|
test('actual SIGKILL after directory publication is recoverable and never assigns the copy to a colliding project', () => box(box => {
|
|
162
162
|
const dir = legacy(box), before = contents(dir), options = reviewed(box)
|
|
163
|
-
const worker = `import {migrateProjectStore} from ${JSON.stringify(new URL('
|
|
163
|
+
const worker = `import {migrateProjectStore} from ${JSON.stringify(new URL('../project-stores.mjs', import.meta.url).href)};migrateProjectStore(process.argv[1],process.argv[2],{...JSON.parse(process.argv[3]),fault:event=>{if(event.phase==='renamed')process.kill(process.pid,'SIGKILL')}})`
|
|
164
164
|
const result = spawnSync(process.execPath, ['--input-type=module', '-e', worker, box.store, box.a, JSON.stringify(options)], { encoding: 'utf8', timeout: 15000 })
|
|
165
165
|
assert.equal(result.signal, 'SIGKILL', result.stderr)
|
|
166
166
|
assert.throws(() => storeDirFor('project', box.a), /migration-needs-recovery/)
|
|
@@ -204,7 +204,7 @@ test('source edits after completed migration are diagnosed and never silently me
|
|
|
204
204
|
}))
|
|
205
205
|
test('local CLI previews and applies with both review hashes without package installation', () => box(box => {
|
|
206
206
|
legacy(box)
|
|
207
|
-
const cli = fileURLToPath(new URL('
|
|
207
|
+
const cli = fileURLToPath(new URL('../scripts/projects.mjs', import.meta.url))
|
|
208
208
|
const run = args => spawnSync(process.execPath, [cli, ...args, '--cwd', box.a], { env: process.env, encoding: 'utf8', timeout: 10000 })
|
|
209
209
|
const preview = run(['migrate'])
|
|
210
210
|
assert.equal(preview.status, 0, preview.stderr)
|