solopup-memory 0.21.0-beta.2 → 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 +37 -0
- package/COMPATIBILITY.md +8 -5
- package/DETECTION.md +139 -0
- package/DREAMING-DESIGN.md +167 -0
- package/README.en.md +35 -6
- package/README.md +44 -7
- package/RELEASE.md +36 -0
- package/SECURITY.md +11 -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 +117 -27
- package/managed-policy.mjs +3 -1
- package/package.json +91 -68
- package/privacy.mjs +25 -0
- 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} +10 -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/test-privacy.mjs +65 -0
- 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,125 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { detectRoot, detectStore } from '../detect.mjs'
|
|
8
|
+
|
|
9
|
+
function store(files = {}) {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-detect-'))
|
|
11
|
+
for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, name), text)
|
|
12
|
+
return dir
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ids(report) {
|
|
16
|
+
return report.findings.map((finding) => finding.id)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('检测:干净库不报错,锚点用折叠比较(标题带标点也能解析)', () => {
|
|
20
|
+
const dir = store({
|
|
21
|
+
'MEMORY.md': '# 记忆索引\n\n## user\n\n- [user] 中文母语 — user.md#身份说明\n',
|
|
22
|
+
'user.md': '## 身份 说明\n\n中文母语写作者。\n',
|
|
23
|
+
})
|
|
24
|
+
try {
|
|
25
|
+
const report = detectStore(dir)
|
|
26
|
+
assert.equal(report.summary.errors, 0)
|
|
27
|
+
assert.ok(!ids(report).includes('I2-missing-anchor'))
|
|
28
|
+
assert.equal(report.metrics.indexLines, 1)
|
|
29
|
+
} finally {
|
|
30
|
+
rmSync(dir, { recursive: true, force: true })
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('检测:坏索引行、缺失主题文件、重复行、冲突签名都会被报出来', () => {
|
|
35
|
+
const dir = store({
|
|
36
|
+
'MEMORY.md': [
|
|
37
|
+
'# 记忆索引',
|
|
38
|
+
'',
|
|
39
|
+
'## project',
|
|
40
|
+
'',
|
|
41
|
+
'- [nope] 类型不合法 — user.md',
|
|
42
|
+
'- [project] 剧本上限 100 集 — missing.md',
|
|
43
|
+
'- [project] 剧本上限 120 集 — project.md',
|
|
44
|
+
'- [project] 剧本上限 120 集 — project.md',
|
|
45
|
+
'',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
'project.md': '## 正文\n\n剧本上限 120 集。\n',
|
|
48
|
+
})
|
|
49
|
+
try {
|
|
50
|
+
const report = detectStore(dir, { scope: 'project' })
|
|
51
|
+
const seen = ids(report)
|
|
52
|
+
assert.ok(seen.includes('I1-invalid-index-line'))
|
|
53
|
+
assert.ok(seen.includes('I2-missing-topic-file'))
|
|
54
|
+
assert.ok(seen.includes('I6-duplicate-line'))
|
|
55
|
+
assert.ok(seen.includes('I7-conflict'))
|
|
56
|
+
assert.equal(report.summary.errors >= 2, true)
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(dir, { recursive: true, force: true })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('检测:预算超限与过期标记语法错误', () => {
|
|
63
|
+
const lines = ['# 记忆索引', '', '## user', '']
|
|
64
|
+
for (let i = 0; i < 302; i += 1) lines.push(`- [user] 条目 ${i} — user.md`)
|
|
65
|
+
const dir = store({ 'MEMORY.md': lines.join('\n'), 'user.md': '# 用户\n' })
|
|
66
|
+
try {
|
|
67
|
+
const report = detectStore(dir)
|
|
68
|
+
const seen = ids(report)
|
|
69
|
+
assert.ok(seen.includes('I8-budget-lines'))
|
|
70
|
+
assert.equal(report.metrics.indexLines, 302)
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(dir, { recursive: true, force: true })
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('检测:根目录会报告空库与内容完全相同的重复库', () => {
|
|
77
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-detect-root-'))
|
|
78
|
+
const projectA = join(root, 'projects', 'v2-a')
|
|
79
|
+
const projectB = join(root, 'projects', 'v2-b')
|
|
80
|
+
const empty = join(root, 'projects', 'v2-empty')
|
|
81
|
+
mkdirSync(projectA, { recursive: true })
|
|
82
|
+
mkdirSync(projectB, { recursive: true })
|
|
83
|
+
mkdirSync(empty, { recursive: true })
|
|
84
|
+
const body = '# 记忆索引\n\n## project\n\n- [project] 同一条 — project.md\n'
|
|
85
|
+
writeFileSync(join(root, 'MEMORY.md'), body)
|
|
86
|
+
writeFileSync(join(projectA, 'MEMORY.md'), body)
|
|
87
|
+
writeFileSync(join(projectB, 'MEMORY.md'), body)
|
|
88
|
+
try {
|
|
89
|
+
const report = detectRoot(root)
|
|
90
|
+
const seen = report.crossFindings.map((finding) => finding.id)
|
|
91
|
+
assert.ok(seen.includes('X1-empty-project-store'))
|
|
92
|
+
assert.ok(seen.includes('X2-duplicate-store'))
|
|
93
|
+
assert.equal(report.stores.length, 4)
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(root, { recursive: true, force: true })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('检测:正文块出现在标题之前会被提示(结构检查)', () => {
|
|
100
|
+
const dir = store({
|
|
101
|
+
'MEMORY.md': '# 记忆索引\n\n## feedback\n\n- [feedback] 先读再改 — feedback.md\n',
|
|
102
|
+
'feedback.md': '[candidate:aaaaaaaa]\n先读再改\n',
|
|
103
|
+
})
|
|
104
|
+
try {
|
|
105
|
+
const report = detectStore(dir)
|
|
106
|
+
assert.ok(ids(report).includes('I14-candidate-without-heading'))
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(dir, { recursive: true, force: true })
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('检测:段落以未完成标点结尾会被标记为截断;列表引导句不误报', () => {
|
|
113
|
+
const dir = store({
|
|
114
|
+
'MEMORY.md': '# 索引\n\n## user\n\n- [user] 一条 — user.md\n',
|
|
115
|
+
'user.md': '# user\n\n## 记录\n\n主要产出:\n\n- 剧本\n- 剪辑\n\n- 2026-10-28|交接文件需自带上下文,\n',
|
|
116
|
+
})
|
|
117
|
+
try {
|
|
118
|
+
const report = detectStore(dir)
|
|
119
|
+
const truncated = report.findings.filter((finding) => finding.id === 'I14-truncated-paragraph')
|
|
120
|
+
assert.equal(truncated.length, 1)
|
|
121
|
+
assert.match(truncated[0].message, /交接文件/)
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(dir, { recursive: true, force: true })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const { planDream } = await import(join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'dream.mjs'))
|
|
10
|
+
const hash = (file) => createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
11
|
+
|
|
12
|
+
function store() {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-dream-'))
|
|
14
|
+
writeFileSync(join(root, 'MEMORY.md'), [
|
|
15
|
+
'# 索引',
|
|
16
|
+
'',
|
|
17
|
+
'## project',
|
|
18
|
+
'',
|
|
19
|
+
'- [project] 重复条目 — project.md',
|
|
20
|
+
'- [project] 重复条目 — project.md',
|
|
21
|
+
'- [project] 指向缺失文件 — missing.md',
|
|
22
|
+
'- [project] 当前版本 v0.2.0 — project.md',
|
|
23
|
+
'',
|
|
24
|
+
].join('\n'))
|
|
25
|
+
writeFileSync(join(root, 'project.md'), '# project\n\n## 一\n\n正文。\n')
|
|
26
|
+
return root
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('做梦 P0:只生成 op 计划,不改任何文件、不调模型', () => {
|
|
30
|
+
const root = store()
|
|
31
|
+
try {
|
|
32
|
+
const before = hash(join(root, 'MEMORY.md'))
|
|
33
|
+
const plan = planDream(root)
|
|
34
|
+
assert.equal(plan.readOnly, true)
|
|
35
|
+
assert.equal(plan.applySupported, false)
|
|
36
|
+
assert.equal(plan.modelCalls, 0)
|
|
37
|
+
const kinds = plan.ops.map((op) => op.kind)
|
|
38
|
+
assert.ok(kinds.includes('merge-duplicate'))
|
|
39
|
+
assert.ok(kinds.includes('repair-reference'))
|
|
40
|
+
assert.ok(kinds.includes('compact-index') || kinds.includes('review-stale'))
|
|
41
|
+
assert.ok(plan.ops.every((op) => /^[0-9a-f]{12}$/.test(op.opId)))
|
|
42
|
+
assert.equal(hash(join(root, 'MEMORY.md')), before)
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(root, { recursive: true, force: true })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('做梦 P1:只自动执行 compact-index,需 reviewed 哈希,可重复执行且不二次改动', async () => {
|
|
49
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-dream-apply-'))
|
|
50
|
+
try {
|
|
51
|
+
const lines = ['# 索引', '', '## project', '']
|
|
52
|
+
for (let index = 0; index < 302; index += 1) lines.push(`- [project] 条目 ${index} 编号 ${index} — project.md`)
|
|
53
|
+
writeFileSync(join(root, 'MEMORY.md'), lines.join('\n') + '\n')
|
|
54
|
+
writeFileSync(join(root, 'project.md'), '# project\n\n## 一\n\n正文。\n')
|
|
55
|
+
const { planDream, applyDream, planHash } = await import(join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'dream.mjs'))
|
|
56
|
+
const before = hash(join(root, 'MEMORY.md'))
|
|
57
|
+
const plan = planDream(root)
|
|
58
|
+
const compact = plan.ops.filter((op) => op.kind === 'compact-index')
|
|
59
|
+
assert.ok(compact.length >= 1, '超限库应产生 compact-index op')
|
|
60
|
+
assert.throws(() => applyDream(plan, 'deadbeef'), /dream-review-hash-mismatch/)
|
|
61
|
+
const result = applyDream(plan, planHash(plan))
|
|
62
|
+
assert.equal(result.applied >= 1, true)
|
|
63
|
+
assert.ok(result.backup !== null && Object.keys(result.backup).length >= 1)
|
|
64
|
+
assert.ok(result.verification.every((row) => row.ok))
|
|
65
|
+
assert.ok(readFileSync(join(root, 'MEMORY.md'), 'utf8') !== readFileSync(Object.values(result.backup)[0] + '/MEMORY.md', 'utf8') || hash(join(root, 'MEMORY.md')) !== before)
|
|
66
|
+
const again = planDream(root)
|
|
67
|
+
const compactAgain = again.ops.filter((op) => op.kind === 'compact-index')
|
|
68
|
+
assert.equal(compactAgain.length, 0, '应用后不应再产生 compact-index op')
|
|
69
|
+
} finally {
|
|
70
|
+
rmSync(root, { recursive: true, force: true })
|
|
71
|
+
}
|
|
72
|
+
})
|
|
@@ -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){
|
|
@@ -40,6 +40,12 @@ test('native approval publishes the unchanged typed candidate; exact replay is a
|
|
|
40
40
|
assert.equal(result.status,'recorded');assert.equal(f.records()[0].summary,proposal.summary);assert.equal(f.requests.length,1)
|
|
41
41
|
assert.equal((await f.tool.execute(proposal,exec)).status,'duplicate');assert.equal(f.requests.length,1)
|
|
42
42
|
}))
|
|
43
|
+
test('unrelated native admission does not disclose saved private records to its reviewer',()=>box({duringStream:(_f,input)=>{
|
|
44
|
+
assert.doesNotMatch(JSON.stringify(input.currentMemory),/1993|4月16|个人档案/)
|
|
45
|
+
}},async f=>{
|
|
46
|
+
writeFixtureMemory({dir:f.root,type:'user',scope:'global',summary:'个人档案:出生于1993年4月16日,女性',meaning:{kind:'claim'}})
|
|
47
|
+
f.owner();const result=await f.tool.execute(proposal,f.call());assert.equal(result.status,'recorded')
|
|
48
|
+
}))
|
|
43
49
|
test('definite rejection leaves no current or pending memory and does not render success',()=>box({state:'unsupported'},async f=>{
|
|
44
50
|
f.owner();const result=await f.tool.execute(proposal,f.call());assert.equal(result.status,'rejected');assert.equal(f.records().length,0);assert.doesNotMatch(f.rendered(result),/已写入/);assert.equal(searchStore(f.root,'长期反馈').hits.length,0)
|
|
45
51
|
}))
|