Files
HologramAgent-DSH-Memory-Port/test/loader.test.js
jianf c6595d2cf0 feat(memory-b): H 收尾 W1–W8 —— 信号转发、写侧校验、建中心入口
五条全部处理。第 5 条确认属实:saveSpaceCentroid 在 lib/ 里零调用点,
真实使用下中心表永久空着,票③ 的「dim unknown」会从边角变成常态。

W1/W5 信号转发(最硬的一条)
- execute(args, exec) → commit(input, {signal}) → embedText(text, signal)
- abort 时按「向量化失败」降级落盘:块写入、vector 留空
- 依据 dsh-tools/lib/types/index.d.ts:120-122

★ 一处我判错了,据实记录
W5 我原本断言「abort 时 isError === false」,实测是 true。查了实现:
dsh-tools/lib/index.js:3408 在工具成功后**再检查一次 signal**,
已 abort 就用 toolAbortedResult 覆盖成功结果。所以 isError 不归本插件控制,
断言它等于和 harness 对抗。改为断言**副作用**(块已写入、vector 为空),
并额外断言 error.info.code === 'ABORTED'。

W2 九个调用点补 await
scripts/checkpoints.js ×4、test/plugin.test.js ×5、test/loader.test.js ×2
(loader 两处保持并发语义用 Promise.all)。
原本绿是因为「无 embedder 时第一个 await 之前就写完 putBlock」——
这个隐式事实一旦被一行 await 打破,九处会静默变成「块没写」,
而且红会出现在离原因很远的地方(靠后面能否召回间接断言)。

W3 空指纹先判后嵌
原来顺序是 await embedText → putBlock 抛 → catch → 再 putBlock,
语义终点对但白付一次网络往返,且与 recall.js:430 的口径不一致。
改为 embed 之前判 space.fingerprint === ''。

W4 写侧维度校验
commit 现在检查 vector.length === space.dimension,不符按 embed failure 处理
(无向量落盘)。不落坏向量的理由:召回侧会按 skippedDim 静默跳过,
用户看到的是「记忆里有这条,但永远召不回来」。

W8 建中心入口(第 5 条)
新增 store.rebuildCentroid(fingerprint),移植 RebuildCentroid
(centroid_store.go:246-320):
  · 只取该空间的带向量行
  · 维度不符跳过不强制
  · 按 L2 归一化后累加均值
  · 均值范数 >= 0.6 才判 anomalous(centroid.go:87)
  · 计入 stats:meanLength / avgPairCosine / skippedRows
并新增 MEAN_THRESHOLD 常量。

★ W8 第一版判据是错的:我用完全共线的向量(meanLength = 1.0000)
  去测 0.6 阈值,把阈值改成 0.95 仍然通过。已改用 ±35° 锥
  (meanLength ≈ 0.879,落在 0.6 与 0.95 之间),并加断言
  「语料必须落在两阈值之间」,否则测试自身就失去区分力。

判据与变异(全部在 /tmp 干净副本上跑)
  W4  维度不符 → 块在、vector=''、fingerprint=''      | 删维度检查 → 红
  W5  abort → 块在、vector=''、elapsed<2s              | 不传 signal → 红(实测 5003ms)
  W6  无 embedder 时 {id,created} 与 putBlock 同形      | —
  W7  空指纹 → embedText 调用 0 次                      | 改为事后判 → 红
  W8  同空间重建、跨空间隔离、坏维度跳过、阈值判定      | 去空间过滤 → 3 条红
                                                        | 去维度过滤 → 1 条红
                                                        | 阈值 0.6→0.95 → 1 条红

未越界
- 四态计数(noVector/unknownSpace/spaceMismatch/embedFailure)留给 F/D
- recallBlocks 仍然同步、未接入 vectorScores(V 组的事)
- 票③「dim 来自中心元数据而非配置」继续有效;saveSpaceCentroid 仍在用
  dim ?? space.dimension,已在注释里标记为 C 组要区分「没建过」与
  「建过但维度取自配置」——声明不是证据

顺序按你的调整记下:H(完工) → V → C → F → D。
C 组准入条件已满足一半:机制(rebuildCentroid)有了,
「谁触发建中心」待 C 组定。

验证
- npm test 99/99(90 + W4/W5/W6/W7 六条 + W8 三条)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
2026-10-30 10:35:41 +08:00

144 lines
5.8 KiB
JavaScript

/**
* Real-loader integration test: mount the plugin the way a dsh profile does.
*
* This is the one test that proves the *packaging* is right — `name`/`inject`/
* `apply` exports, the `defineTool` contract, and `systemPrompt.context` all
* survive an actual `cordis-plugin-loader` mount rather than a hand-built app.
*
* If `cordis-plugin-loader` cannot be resolved (bare install, no dsh), the test
* skips rather than fails: the plugin itself does not depend on the loader.
*
* @module dsh-memory/test/loader
*/
import assert from 'node:assert/strict'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { Context } from '@deepseek-ai/cordis'
import { SystemPrompt } from '@deepseek-ai/dsh-system-prompt'
import { ToolRuntime } from '@deepseek-ai/dsh-tools'
import * as plugin from '../lib/index.js'
/** @returns {Promise<unknown>} the loader module, or null when unavailable */
async function tryLoadLoader() {
try {
return await import('@deepseek-ai/cordis-plugin-loader')
} catch {
return null
}
}
test('plugin exports the shape the cordis loader requires', () => {
// A loader entry is only mountable when these three exist.
assert.equal(typeof plugin.apply, 'function', 'apply(ctx, config) is required')
assert.equal(typeof plugin.name, 'string', 'name is required for diagnostics')
assert.ok(Array.isArray(plugin.inject), 'inject must be an array')
assert.deepEqual(plugin.inject, ['systemPrompt', 'tools'])
// The default export must be the same plugin (loaders unwrap `default`).
assert.equal(plugin.default.name, plugin.name)
assert.equal(plugin.default.apply, plugin.apply)
})
test('mounting twice on the same app fails loudly (service name is taken)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-memory-dup-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
plugin.apply(ctx, { path: join(dir, 'a.db') })
// 'stablemem' is provided by the first instance; cordis refuses a second
// implementation in the same realm — which is exactly why a preset that
// wants its own implementation must isolate the service name
// (docs/shadow-context.md §5.2).
assert.throws(
() => plugin.apply(ctx, { path: join(dir, 'b.db') }),
/stablemem/,
'duplicate service registration must be rejected',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})
test('an isolated realm can host its own stablemem (the preset route)', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dsh-memory-iso-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const outer = plugin.apply(ctx, { path: join(dir, 'outer.db') })
// A preset subtree isolates the service name and mounts a second instance.
// Prompt-context names and tool names are NOT partitioned by isolate()
// (dsh-system-prompt/lib/index.js:204, dsh-tools duplicate check), so a
// second instance needs a distinct tool name. The prompt-context name is
// derived from the db path automatically.
const realm = ctx.isolate('stablemem')
const inner = plugin.apply(realm, {
path: join(dir, 'inner.db'),
toolName: 'stablemem_commit_alt',
})
// Both awaits are required: commit() embeds (async) before writing, and
// relying on "no embedder means it happens synchronously" is exactly the
// fragile assumption this guards against. Promise.all keeps them concurrent,
// which is what this test is about (two isolated realms writing in parallel).
await Promise.all([
outer.commit({ text: 'outer-only 端口 1111' }),
inner.commit({ text: 'inner-only 端口 2222' }),
])
assert.equal(outer.store.countBlocks(), 1, 'outer store isolated')
assert.equal(inner.store.countBlocks(), 1, 'inner store isolated')
assert.notEqual(outer.store.path, inner.store.path, 'separate databases')
// The isolated service is reachable through the realm context, and the two
// contexts resolve DIFFERENT implementations — that is the whole point.
const realmService = realm.stablemem
// cordis hands out a PROXY, so identity differs from the instance we built;
// compare behaviour, not references. (This proxy is also why StableMem keeps
// ordinary fields instead of #private ones.)
assert.equal(realmService.store.countBlocks(), 1, 'realm sees only its own block')
assert.equal(realmService.store.path, inner.store.path, 'realm resolves the inner instance')
assert.equal(ctx.stablemem.store.path, outer.store.path, 'root resolves the outer instance')
assert.equal(ctx.stablemem.store.countBlocks(), 1, 'outer store untouched by the realm')
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})
test('a real cordis loader can mount the plugin by specifier', async (t) => {
const loaderModule = await tryLoadLoader()
if (loaderModule === null) {
t.skip('cordis-plugin-loader not installed in this environment')
return
}
const dir = mkdtempSync(join(tmpdir(), 'dsh-memory-loader-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const pluginSpecifier = new URL('../lib/index.js', import.meta.url).href
const fiber = ctx.plugin(
{
name: pluginSpecifier,
inject: plugin.inject,
apply: plugin.apply,
},
{ path: join(dir, 'loaded.db') },
)
await fiber.await()
assert.ok(ctx.stablemem, 'the loaded plugin registered ctx.stablemem')
assert.ok(
ctx.tools.schemas().some((schema) => schema.name === 'stablemem_commit'),
'the loaded plugin registered its tool',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})