Files
HologramAgent-DSH-Memory-Port/test/phase-b-h.test.js

3530 lines
155 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Phase B · group H — HTTP embedding adapter.
*
* The adapter is the only transport-aware file (`G5` enforces that). These tests
* pin its three contracts:
*
* H1 request body matches HologramAgent's protocol field-for-field
* H2 both response shapes (`embedding`, `data[0].embedding`) are accepted
* H3 a non-2xx / malformed / wrong-dimension response is a HARD error, never
* an empty vector — HM's recall layer turns that into a degradation, but
* the adapter must not swallow it (`block_recall.go:135-138`)
* H4 timeout is configurable and surfaces as an error
* H5 zero new dependencies (asserted by the import guard in G2b; here we pin
* that the module resolves using only `node:` builtins)
* H6 response body is capped
* + fingerprint derivation: endpoint hash, normalisation, override
*
* Network calls are served by an injected `fetch` so these run offline and
* deterministically.
*
* @module dsh-memory/test/phase-b-h
*/
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 { DatabaseSync } from 'node:sqlite'
import { createHttpEmbedder, deriveFingerprint, normalizeEndpoint } from '../lib/embed/http.js'
import { BlockStore } from '../lib/store.js'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { degradationNotice, recallBlocks, recallBlocksHybrid, renderRecall } from '../lib/recall.js'
/**
* Run `body` with `globalThis.fetch` replaced by a stub.
* @param {(calls: Array<{url: string, init: object}>) => Promise<Response>|Response} handler
* @param {(calls: Array<{url: string, init: object}>) => Promise<void>} body
*/
async function withFetchStub(handler, body) {
const original = globalThis.fetch
const calls = []
globalThis.fetch = async (url, init) => {
calls.push({ url: String(url), init })
return handler(calls)
}
try {
await body(calls)
} finally {
globalThis.fetch = original
}
}
/** A minimal Response stand-in (Node's real one is fine, but keep it explicit). */
function jsonResponse(payload, status = 200) {
const text = JSON.stringify(payload)
return {
ok: status >= 200 && status < 300,
status,
text: async () => text,
}
}
const ENDPOINT = 'http://127.0.0.1:18999/embed'
// ── H1: request body matches HA's protocol ────────────────────────────────
test('H1: request body carries model/modality/side/text exactly as HA does', async () => {
await withFetchStub(
() => jsonResponse({ embedding: [0.1, 0.2, 0.3] }),
async (calls) => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3, model: 'jina-v5' })
await embedder.embedText('值班室分机号 4324')
assert.equal(calls.length, 1)
const body = JSON.parse(calls[0].init.body)
assert.equal(body.model, 'jina-v5')
assert.equal(body.modality, 'text', 'modality is always text in this slice')
assert.equal(body.side, 'query', 'query-side encoding')
assert.equal(body.text, '值班室分机号 4324')
// Fields HA omits for text input must not appear.
assert.equal(body.data, undefined, 'text input carries no base64 data')
assert.equal(body.mime, undefined)
assert.equal(calls[0].init.method, 'POST')
},
)
})
test('H1b: apiKey becomes an Authorization bearer header; absent key sends none', async () => {
await withFetchStub(
() => jsonResponse({ embedding: [1, 2, 3] }),
async (calls) => {
await createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3, apiKey: 'sekret' }).embedText('x')
assert.equal(calls[0].init.headers.Authorization, 'Bearer sekret')
await createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 }).embedText('x')
assert.equal(calls[1].init.headers.Authorization, undefined, 'no key → no header')
},
)
})
// ── H2: both response shapes ──────────────────────────────────────────────
test('H2: both response shapes are accepted', async () => {
await withFetchStub(
(calls) =>
calls.length === 1
? jsonResponse({ embedding: [1, 2, 3] })
: jsonResponse({ data: [{ embedding: [4, 5, 6] }] }),
async () => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
assert.deepEqual(await embedder.embedText('a'), [1, 2, 3])
assert.deepEqual(await embedder.embedText('b'), [4, 5, 6])
},
)
})
// ── H3: failure is hard, never an empty vector ────────────────────────────
test('H3: non-2xx throws (does not return an empty vector)', async () => {
await withFetchStub(
() => jsonResponse({ error: 'nope' }, 503),
async () => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
await assert.rejects(() => embedder.embedText('x'), /503/)
},
)
})
test('H3b: a wrong-dimension response throws rather than truncating or padding', async () => {
await withFetchStub(
() => jsonResponse({ embedding: [1, 2] }),
async () => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
await assert.rejects(() => embedder.embedText('x'), /dimension 2, want 3/)
},
)
})
test('H3c: a payload with no embedding array throws', async () => {
await withFetchStub(
() => jsonResponse({ data: [] }),
async () => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
await assert.rejects(() => embedder.embedText('x'), /no embedding array/)
},
)
})
test('H3d: the adapter failure surfaces as embed_error in hybrid recall', async () => {
await withFetchStub(
() => jsonResponse({}, 500),
async () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324' })
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
const hits = await recallBlocksHybrid(store, embedder, '4324', { topK: 3 })
assert.equal(hits.degraded, 'embed_error', 'a dead endpoint degrades, never throws')
assert.ok(hits.length >= 1, 'and the symbol path still answers')
} finally {
store.close()
}
},
)
})
// ── H4: timeout ──────────────────────────────────────────────────────────
test('H4: a request that never settles is aborted by the configured timeout', async () => {
const original = globalThis.fetch
globalThis.fetch = (url, init) =>
new Promise((_resolve, reject) => {
init.signal?.addEventListener('abort', () => reject(new Error('aborted')))
})
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3, timeoutMs: 25 })
await assert.rejects(() => embedder.embedText('x'), /aborted/)
} finally {
globalThis.fetch = original
}
})
// ── H6: response cap ─────────────────────────────────────────────────────
test('H6: an oversized response body is rejected', async () => {
const huge = 'x'.repeat(4 * 1024 * 1024 + 10)
await withFetchStub(
() => ({ ok: true, status: 200, text: async () => huge }),
async () => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
await assert.rejects(() => embedder.embedText('x'), /response exceeds/)
},
)
})
// ── fingerprint ──────────────────────────────────────────────────────────
test('F-fp1: fingerprint is http:<model>:<dim>:<8 hex of the normalised endpoint>', () => {
const fp = deriveFingerprint({ endpoint: ENDPOINT, model: 'jina-v5', dimension: 2048 })
assert.match(fp, /^http:jina-v5:2048:[0-9a-f]{8}$/, fp)
})
test('F-fp2: normalisation collapses cosmetic differences', () => {
const base = normalizeEndpoint('http://Host.EXAMPLE:80/embed')
assert.equal(base, 'http://host.example/embed')
for (const variant of [
'HTTP://host.example/embed',
'http://host.example/embed/',
' http://host.example:80/embed ',
'http://HOST.example:80/embed',
]) {
assert.equal(
normalizeEndpoint(variant),
base,
`"${variant}" must normalise to the same fingerprint input`,
)
}
// ...and the fingerprint agrees.
const a = deriveFingerprint({ endpoint: 'http://host.example:80/embed', dimension: 8 })
const b = deriveFingerprint({ endpoint: 'HTTP://HOST.example/embed/', dimension: 8 })
assert.equal(a, b)
})
test('F-fp3: a different endpoint changes the fingerprint (the bug HA has)', () => {
// HA derives `http:<model>:<dim>` and would call these the same space.
const a = deriveFingerprint({ endpoint: 'http://10.0.0.1:9000/embed', model: 'm', dimension: 8 })
const b = deriveFingerprint({ endpoint: 'http://10.0.0.2:9000/embed', model: 'm', dimension: 8 })
assert.notEqual(a, b, 'different deployments of the same model must NOT share a fingerprint')
})
test('F-fp4: a non-default port is significant; a default port is not', () => {
const withPort = deriveFingerprint({ endpoint: 'http://h:8080/embed', dimension: 8 })
const noPort = deriveFingerprint({ endpoint: 'http://h/embed', dimension: 8 })
const explicit80 = deriveFingerprint({ endpoint: 'http://h:80/embed', dimension: 8 })
assert.notEqual(withPort, noPort)
assert.equal(explicit80, noPort, ':80 is the http default and must not split the space')
})
test('F-fp5: an explicit fingerprint override wins', () => {
const fp = deriveFingerprint({
endpoint: ENDPOINT,
dimension: 8,
fingerprint: 'custom-space-v7',
})
assert.equal(fp, 'custom-space-v7')
})
test('F-fp6: info() is synchronous and stable across calls', () => {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 8, model: 'm' })
const first = embedder.info()
assert.equal(typeof first.fingerprint, 'string')
assert.equal(first.dimension, 8)
assert.deepEqual(embedder.info(), first, 'the fingerprint must not drift between calls')
})
// ── config validation ────────────────────────────────────────────────────
test('H0: construction rejects a missing endpoint or a bad dimension', () => {
assert.throws(() => createHttpEmbedder({ endpoint: '', dimension: 8 }), /requires an endpoint/)
assert.throws(() => createHttpEmbedder({ endpoint: ENDPOINT, dimension: 0 }), /positive integer dimension/)
assert.throws(() => createHttpEmbedder({ endpoint: ENDPOINT, dimension: 1.5 }), /positive integer dimension/)
})
// ── ticket ②: the two tables must hold the SAME fingerprint string ────────
//
// HA reads the centroid with `LoadCentroid(q.Fingerprint, ...)`
// (block_recall.go:92). If `memory_blocks.fingerprint` and
// `graph_centroid.fingerprint` ever disagree on spelling, that lookup silently
// misses — centering never applies, nothing errors, and scores just quietly get
// worse. This is the hardest class of bug to notice, so it is pinned directly.
test('ticket②: block fingerprint and centroid fingerprint are byte-identical', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3, model: 'jina-v5' })
const fp = embedder.info().fingerprint
for (let i = 0; i < 5; i += 1) {
store.putBlock({ text: `块 ${i}`, vector: [i, i + 1, i + 2], fingerprint: fp })
}
store.saveCentroid({
fingerprint: fp,
dim: 3,
vectorCount: 5,
mean: [0.1, 0.2, 0.3],
isAnomalous: true,
stats: { note: 'test' },
})
// Everything the blocks recorded ...
const spaces = store.blockFingerprints()
assert.equal(spaces.length, 1)
assert.equal(spaces[0].fingerprint, fp, 'blocks must carry the embedder fingerprint verbatim')
// ... must be exactly what the centroid row is keyed by.
const centroid = store.loadCentroid(spaces[0].fingerprint)
assert.ok(centroid, 'centroid must be findable by the blocks\' fingerprint')
assert.equal(centroid.fingerprint, spaces[0].fingerprint, 'the two tables must agree byte-for-byte')
} finally {
store.close()
}
})
test('ticket② mutation: a re-derived centroid fingerprint misses the lookup', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3, model: 'jina-v5' })
const fp = embedder.info().fingerprint
store.putBlock({ text: 'x', vector: [1, 2, 3], fingerprint: fp })
// The mutation: the centroid is stored under a differently-spelled key —
// e.g. after a normalization change, or hand-built.
store.saveCentroid({
fingerprint: `${fp} `, // one trailing space is enough
dim: 3,
vectorCount: 1,
mean: [0, 0, 0],
isAnomalous: true,
})
// The block's own fingerprint must NOT find it. Silent, no throw — which is
// precisely why the equality assertion above has to exist.
assert.equal(store.loadCentroid(fp), undefined, 'a one-character difference must miss')
assert.ok(store.loadCentroid(`${fp} `), 'and the mis-spelled key does resolve')
} finally {
store.close()
}
})
// ── F5: empty fingerprint is rejected at WRITE time (fail-closed) ────────
//
// Ruling: fail-closed, not HA's fail-open. A block carrying a vector but no
// space identity can never be safely compared to anything — admitting it and
// hoping a downstream layer catches it is strictly weaker than refusing it here.
// HA passes such blocks through (`block_recall.go:140` is three `&&`, so an empty
// block fingerprint short-circuits the skip). That is the behaviour this project
// deliberately does NOT mirror: security semantics over bug-for-bug parity.
//
// The existing test asserted only the absent-field case. These cover the shapes a
// caller actually produces: omitted, empty string, whitespace, non-string.
test('F5: a block with a vector but no usable fingerprint is refused', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const rejected = [
{ label: 'fingerprint omitted', input: { text: 'x', vector: [1, 2, 3] } },
{ label: 'fingerprint empty string', input: { text: 'x', vector: [1, 2, 3], fingerprint: '' } },
{ label: 'fingerprint undefined', input: { text: 'x', vector: [1, 2, 3], fingerprint: undefined } },
{ label: 'fingerprint is a number', input: { text: 'x', vector: [1, 2, 3], fingerprint: 7 } },
{ label: 'fingerprint is null', input: { text: 'x', vector: [1, 2, 3], fingerprint: null } },
]
for (const { label, input } of rejected) {
assert.throws(
() => store.putBlock(input),
/requires the vector-space fingerprint/,
`${label}: an unlabelled vector must be refused, not silently accepted`,
)
}
// Nothing was written by any of the rejected calls.
assert.equal(store.countBlocks(), 0, 'a refused block must not land')
// The two legitimate shapes still work.
assert.equal(store.putBlock({ text: 'plain' }).created, true, 'no vector, no fingerprint: fine')
assert.equal(
store.putBlock({ text: 'labelled', vector: [1, 2, 3], fingerprint: 'fp-a' }).created,
true,
'vector + fingerprint: fine',
)
assert.deepEqual(
store.blockFingerprints().map((row) => row.fingerprint),
['fp-a'],
'only the labelled block contributes a space',
)
} finally {
store.close()
}
})
test('F5†: removing the write-side guard lets an unlabelled vector through', async () => {
await withMutatedCopy(
'store.js',
[[
` if (vectorJSON !== '' && (typeof fingerprint !== 'string' || fingerprint === '')) {
throw new Error('dsh-memory: a block with a vector requires the vector-space fingerprint')
}`,
` // MUTATED: no write-side fingerprint guard (HA's fail-open).`,
]],
async (mod) => {
const store = new mod.BlockStore({ path: ':memory:' }).open()
try {
// Under the mutation the very call F5 expects to throw succeeds. Assert
// BOTH halves so this test can never pass for the wrong reason:
// 1. the mutation really does admit the block, and
// 2. F5's own assertion (`assert.throws`) would therefore fail on it.
let accepted = false
try {
store.putBlock({ text: 'x', vector: [1, 2, 3] })
accepted = true
} catch {
accepted = false
}
assert.equal(accepted, true, 'mutation must really admit an unlabelled vector')
const row = store.allBlocks(1)[0]
assert.equal(row.vector !== '', true, 'and store the vector')
assert.equal(row.fingerprint, '', 'with no space identity — the fail-open state')
// Now emulate F5's assertion against the mutated module: it must reject.
assert.throws(
() => {
if (accepted) throw new Error('unlabelled vector was accepted')
},
/was accepted/,
'under the mutation, F5 (which requires a throw) fails — so F5† proves F5 is load-bearing',
)
} finally {
store.close()
}
},
)
})
test('ticket②: re-committing without a vector must not erase the stored one', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
const fp = embedder.info().fingerprint
store.putBlock({ text: 'same text', vector: [1, 2, 3], fingerprint: fp })
// A later commit of the same content with no vector (e.g. embedder offline).
store.putBlock({ text: 'same text' })
const spaces = store.blockFingerprints()
assert.equal(spaces.length, 1, 'the earlier vector and fingerprint must survive')
assert.equal(spaces[0].embedded, 1)
} finally {
store.close()
}
})
// ── migration: a phase-A database must keep working ───────────────────────
//
// Phase A shipped `CREATE TABLE IF NOT EXISTS` with no `vector`/`fingerprint`
// columns. `open()` re-runs that DDL, which is a SILENT no-op on an existing
// table. So a database created by the phase-A build opens fine, recalls fine via
// FTS, and then throws on the FIRST commit:
//
// PUTBLOCK THREW: table memory_blocks has no column named vector
//
// The memory store becomes read-only with no error at load time. Worse than the
// G5 bug: a false alarm gets noticed, this one does not.
/** Build a database with exactly the phase-A schema (no vector/fingerprint). */
function buildPhaseADatabase(path) {
const db = new DatabaseSync(path)
db.exec(`CREATE TABLE IF NOT EXISTS memory_blocks (
id TEXT PRIMARY KEY,
modality TEXT NOT NULL,
text_content TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
scene TEXT NOT NULL DEFAULT '',
semantic_type TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL)`)
db.exec(`CREATE TABLE IF NOT EXISTS memory_block_edges (
id INTEGER PRIMARY KEY AUTOINCREMENT, source_kind TEXT NOT NULL, source_id TEXT NOT NULL,
target_kind TEXT NOT NULL, target_id TEXT NOT NULL, type TEXT NOT NULL,
confidence REAL NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL)`)
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_blocks_fts
USING fts5(text_content, block_id UNINDEXED, tokenize='unicode61 remove_diacritics 2')`)
db.exec(`INSERT INTO memory_blocks (id, modality, text_content, source, created_at, updated_at)
VALUES ('blk_legacy', 'text', '值班室分机号 4324', 'legacy', 1, 1)`)
db.exec(`INSERT INTO memory_blocks_fts (text_content, block_id) VALUES ('值班室分机号 4324', 'blk_legacy')`)
db.close()
}
test('MIG1: a phase-A database gains the vector columns and stays writable', () => {
const dir = mkdtempSync(join(tmpdir(), 'mig-'))
const path = join(dir, 'phase-a.db')
try {
buildPhaseADatabase(path)
const store = new BlockStore({ path }).open()
// 1. Columns exist after open.
const columns = store.columns('memory_blocks')
assert.ok(columns.includes('vector'), `missing vector column; have ${columns.join(',')}`)
assert.ok(columns.includes('fingerprint'), `missing fingerprint column; have ${columns.join(',')}`)
// 2. The legacy block is still readable, and still findable by FTS.
const legacy = store.getBlock('blk_legacy')
assert.ok(legacy, 'the phase-A block must survive')
assert.equal(legacy.text_content, '值班室分机号 4324')
assert.equal(legacy.vector, '', 'a pre-existing block has no vector, not a fabricated one')
const hits = store.searchFts('"4324"', 5)
assert.ok(hits.length >= 1, 'FTS must still find the legacy block')
// 3. Writing works — this is what used to throw.
const { created } = store.putBlock({ text: '迁移之后的新块 8499' })
assert.equal(created, true, 'commit must work after migration')
// 4. The version is stamped.
assert.equal(store.userVersion(), 1, 'schema version must be recorded')
// 5. Re-opening is idempotent (migration must not re-run destructively).
store.close()
const again = new BlockStore({ path }).open()
assert.equal(again.userVersion(), 1)
assert.equal(again.countBlocks(), 2)
again.close()
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
test('MIG2: a fresh database is created at version 1', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
assert.equal(store.userVersion(), 1, 'a new database starts current, not at 0')
} finally {
store.close()
}
})
// NOTE on what used to be here: a test named "MIG1 mutation" that built a raw
// phase-A database and asserted on `DatabaseSync` directly. It touched no `lib/`
// code, so it could never turn red when the implementation changed — a
// demonstration, not a guard. It has been removed rather than kept as a test
// that inflates the count while verifying nothing.
//
// The real guard for the ALTER is MIG1 above: commenting out the
// `ADD COLUMN vector` in migrate() makes it fail with
// `missing vector column; have id,modality,...`. Verified by mutation, not
// asserted by a lookalike.
// ── MIG3: the centroid table must survive migration ──────────────────────
//
// The migration drops phase A's `graph_centroid` and relies on DDL to rebuild
// it — but DDL ran BEFORE the migration, so on a pre-existing database that
// `CREATE TABLE IF NOT EXISTS` was a no-op and nothing rebuilt the table.
// Result: open succeeds, `user_version` reads 1, `putBlock` writes, FTS recalls
// — and only `saveCentroid` throws `no such table`. The narrowest, latest-possible
// failure, which is why MIG1 (which only checks memory_blocks) could not see it.
//
// Fix: rebuild the centroid table inside the migration, right where it is dropped.
/** Build a phase-A-shaped database including the OLD centroid table. */
function buildPhaseAWithCentroid(path, centroidRows = 0) {
const db = new DatabaseSync(path)
db.exec(`CREATE TABLE IF NOT EXISTS memory_blocks (
id TEXT PRIMARY KEY, modality TEXT NOT NULL, text_content TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '', scene TEXT NOT NULL DEFAULT '',
semantic_type TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`)
// Phase A's centroid shape: (fingerprint, dim, count, sum_json, updated_at)
db.exec(`CREATE TABLE IF NOT EXISTS graph_centroid (
fingerprint TEXT PRIMARY KEY, dim INTEGER NOT NULL, count INTEGER NOT NULL,
sum_json TEXT NOT NULL, updated_at INTEGER NOT NULL)`)
for (let i = 0; i < centroidRows; i += 1) {
db.exec(`INSERT INTO graph_centroid (fingerprint, dim, count, sum_json, updated_at)
VALUES ('fp${i}', 3, 1, '[0,0,0]', 1)`)
}
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_blocks_fts
USING fts5(text_content, block_id UNINDEXED, tokenize='unicode61 remove_diacritics 2')`)
db.close()
}
test('MIG3: a phase-A database can write and read a centroid after migration', () => {
const dir = mkdtempSync(join(tmpdir(), 'mig3-'))
const path = join(dir, 'a.db')
try {
buildPhaseAWithCentroid(path, 0)
const store = new BlockStore({ path }).open()
// The table must exist with phase-B columns.
const cols = store.columns('graph_centroid')
for (const required of ['fingerprint', 'dim', 'vector_count', 'vector', 'stats', 'is_anomalous', 'built_at']) {
assert.ok(cols.includes(required), `graph_centroid missing "${required}"; have ${cols.join(',')}`)
}
// And it must be usable — this is what used to throw.
store.saveCentroid({
fingerprint: 'fp-p0',
dim: 3,
vectorCount: 2,
mean: [0.1, 0.2, 0.3],
isAnomalous: true,
})
const loaded = store.loadCentroid('fp-p0')
assert.ok(loaded, 'the centroid must round-trip')
assert.equal(loaded.fingerprint, 'fp-p0')
assert.equal(loaded.isAnomalous, true)
assert.deepEqual(loaded.mean, [0.1, 0.2, 0.3])
store.close()
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
test('MIG3b: a phase-A centroid WITH rows is renamed aside, not fatal and not lost', () => {
// Phase A never wrote a centroid, so a populated old table is a "should not
// happen" branch. Refusing to open the whole database punishes the user for a
// shape the plugin itself could not have produced by normal use; dropping the
// rows silently would lose data. Rename aside keeps both properties.
const dir = mkdtempSync(join(tmpdir(), 'mig3b-'))
const path = join(dir, 'b.db')
try {
buildPhaseAWithCentroid(path, 2)
const store = new BlockStore({ path }).open()
assert.equal(store.userVersion(), 1, 'the database must still open')
// The new shape is usable immediately.
store.saveCentroid({ fingerprint: 'fp-new', dim: 3, vectorCount: 1, mean: [0, 0, 0], isAnomalous: false })
assert.ok(store.loadCentroid('fp-new'))
// The old data is preserved under a renamed table.
const legacy = store.tables().filter((name) => name.startsWith('graph_centroid_phase_a'))
assert.equal(legacy.length, 1, `expected one renamed legacy table, saw ${JSON.stringify(store.tables())}`)
assert.equal(store.raw().prepare(`SELECT COUNT(*) AS n FROM "${legacy[0]}"`).get().n, 2, 'rows preserved')
store.close()
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
// ── MIG4: a crash between RENAME and the version stamp must be recoverable ─
//
// The legacy name is second-resolution (`graph_centroid_phase_a_<14 digits>`).
// If the rename succeeds but the process dies before `PRAGMA user_version`
// commits, the next open() re-enters the rename branch and
// `ALTER TABLE ... RENAME TO <same name>` would throw `table already exists`,
// making the database unopenable forever. Improbable, but the recovery path is
// cheap and the failure is total.
test('MIG4: a leftover legacy table from an interrupted migration does not block open', () => {
// Mechanism (verified, not assumed): `pragma_table_info` returns zero rows for
// a table that does not exist, so once the old table has been renamed away the
// migration branch is skipped entirely on the retry — no second RENAME. Note
// this holds regardless of DDL ordering; reversing open()'s order keeps the
// suite green, which is how the DDL-ordering explanation was ruled out.
const dir = mkdtempSync(join(tmpdir(), 'mig4-'))
const path = join(dir, 'a.db')
try {
buildPhaseAWithCentroid(path, 2)
// Simulate the crash: the old table was already renamed, but the new one was
// never built and the version was never stamped.
const raw = new DatabaseSync(path)
raw.exec('ALTER TABLE graph_centroid RENAME TO graph_centroid_phase_a_20260101000000')
assert.equal(raw.prepare('PRAGMA user_version').get().user_version, 0)
raw.close()
// Now open normally: it must not throw, must rebuild the live table, and must
// not disturb the already-renamed rows.
const store = new BlockStore({ path }).open()
assert.equal(store.userVersion(), 1)
store.saveCentroid({ fingerprint: 'fp-r', dim: 3, vectorCount: 1, mean: [0, 0, 0], isAnomalous: false })
assert.ok(store.loadCentroid('fp-r'), 'the rebuilt table must be usable')
const legacyTables = store.tables().filter((name) => name.startsWith('graph_centroid_phase_a'))
assert.equal(legacyTables.length, 1, `expected exactly one legacy table, saw ${JSON.stringify(legacyTables)}`)
assert.equal(
store.raw().prepare(`SELECT COUNT(*) AS n FROM "${legacyTables[0]}"`).get().n,
2,
'the pre-crash rows must be untouched',
)
store.close()
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
// ── ticket② (strong form): ONE derivation, two tables ─────────────────────
//
// The test above passes a string to both write paths, so it proves only that
// identical inputs produce identical keys — not that the plugin is incapable of
// deriving two different ones. These tests go through the service surface, where
// the fingerprint comes from `space()` and cannot be supplied by the caller.
test('ticket②-strong: service-driven commit and centroid share one fingerprint', async () => {
const { apply: applyMemory } = await import('../lib/index.js')
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'fp-strong-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
await withFetchStub(
() => jsonResponse({ embedding: [0.5, 0.25, 0.125] }),
async () => {
try {
const mem = applyMemory(ctx, {
path: join(dir, 'm.db'),
embed: { endpoint: ENDPOINT, dimension: 3, model: 'jina-v5' },
})
// Commit through the service (embeds + stamps), three blocks.
for (let i = 0; i < 3; i += 1) {
const r = await mem.commit({ text: `服务路径块 ${i}` })
assert.equal(r.embedded, true, 'a configured space must embed')
}
// The centroid comes from the space object; no fingerprint is passed in.
const { fingerprint: centroidKey } = mem.saveSpaceCentroid({
vectorCount: 3,
mean: [0.5, 0.25, 0.125],
isAnomalous: true,
})
// Set assertion, exactly as agreed: the distinct block fingerprints and
// the centroid key must be the same single value.
const blockSide = mem.store.raw()
.prepare('SELECT DISTINCT fingerprint FROM memory_blocks')
.all()
.map((row) => row.fingerprint)
const centroidSide = mem.store.raw()
.prepare('SELECT fingerprint FROM graph_centroid')
.all()
.map((row) => row.fingerprint)
assert.deepEqual(blockSide, [centroidKey], 'blocks must carry exactly the centroid key')
assert.deepEqual(centroidSide, [centroidKey], 'the centroid table holds exactly that key')
assert.ok(mem.store.loadCentroid(blockSide[0]), 'lookup by the block-side string must hit')
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
test('ticket②-strong mutation: re-deriving in the centroid path must break the key', async () => {
// The exact failure the constraint forbids: a second derivation with a
// different normalisation (here: a trailing slash survives) yields a different
// string, and the lookup silently misses.
const { apply: applyMemory } = await import('../lib/index.js')
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'fp-mut-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
await withFetchStub(
() => jsonResponse({ embedding: [1, 2, 3] }),
async () => {
try {
const mem = applyMemory(ctx, {
path: join(dir, 'm.db'),
embed: { endpoint: ENDPOINT, dimension: 3, model: 'jina-v5' },
})
await mem.commit({ text: 'a' })
// Mutation: the centroid path derives its own key from a *different*
// spelling that normalisation does NOT collapse — a different query
// string is a genuinely different endpoint (this slice deliberately does
// not reorder query params, because that difference is usually real).
const rawKey = deriveFingerprint({
endpoint: `${ENDPOINT}?tenant=b`,
dimension: 3,
model: 'jina-v5',
})
const spaceKey = mem.space().fingerprint
assert.notEqual(rawKey, spaceKey, 'a second derivation with different input must differ')
mem.store.saveCentroid({
fingerprint: rawKey,
dim: 3,
vectorCount: 1,
mean: [0, 0, 0],
isAnomalous: true,
})
// Blocks can no longer find it: silent, no throw. That is precisely why
// the single-source rule has to be structural.
assert.equal(mem.store.loadCentroid(spaceKey), undefined, 'the block-side key must miss')
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
test('ticket②-strong: normalisation makes the trailing-slash variant a NON-issue', () => {
// The mutation above only works because it bypasses normalisation. Going
// through the real derivation, `…/embed` and `…/embed/` are one space.
const a = deriveFingerprint({ endpoint: 'http://h:9000/embed', dimension: 3 })
const b = deriveFingerprint({ endpoint: 'http://h:9000/embed/', dimension: 3 })
assert.equal(a, b, 'normalisation must collapse the slash so config edits do not split a space')
})
// ── ticket③ boundary: unknown dimension is null, never 0 ─────────────────
//
// When the centroid table has no row for a space that DOES have embedded blocks,
// the dimension is unknown. Encoding that as 0 would be a lie that reads like a
// real answer, and downstream code comparing `dim === 0` would take the wrong
// branch. The state belongs to diagnostics only: recall must be unaffected, and
// `loadCentroid` must not throw for a missing row (HA treats a missing /
// under-sampled centroid as a normal degradation, centroid_store.go:144-184).
test('ticket③: a missing centroid row yields undefined, and reports null — not 0', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
const fp = embedder.info().fingerprint
store.putBlock({ text: '有向量但没建中心', vector: [1, 2, 3], fingerprint: fp })
// loadCentroid must be a normal "absent", not an error path.
assert.equal(store.loadCentroid(fp), undefined, 'a missing centroid is undefined, not a throw')
// Diagnostic projection: the space is known, its dimension is NOT.
const spaces = store.blockFingerprints()
assert.equal(spaces.length, 1)
const centroid = store.loadCentroid(spaces[0].fingerprint)
const dim = centroid === undefined ? null : centroid.dim
assert.equal(dim, null, 'unknown dimension must be null')
assert.notEqual(dim, 0, 'never encode unknown as 0 — 0 reads like a real answer')
assert.equal(spaces[0].embedded, 1, 'the blocks are known to be embedded')
} finally {
store.close()
}
})
test('ticket③: a numeric dimension is reported only when a centroid row exists', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
const fp = embedder.info().fingerprint
store.putBlock({ text: 'x', vector: [1, 2, 3], fingerprint: fp })
store.saveCentroid({ fingerprint: fp, dim: 3, vectorCount: 1, mean: [0, 0, 0], isAnomalous: false })
const centroid = store.loadCentroid(fp)
assert.equal(centroid.dim, 3, 'a real dimension comes from the centroid metadata, not from length(vector)')
} finally {
store.close()
}
})
test('ticket③: a space with no centroid does not change recall behaviour', async () => {
// Group H must not leak vector logic into the recall path: a space whose
// centroid is missing behaves exactly like any other degraded case.
const store = new BlockStore({ path: ':memory:' }).open()
try {
const embedder = createHttpEmbedder({ endpoint: ENDPOINT, dimension: 3 })
store.putBlock({ text: '值班室分机号 4324' })
const hits = await recallBlocksHybrid(store, embedder, '4324', { topK: 3 })
assert.ok(hits.length >= 1, 'symbol recall still answers')
assert.equal(store.loadCentroid('whatever'), undefined, 'no centroid row, no throw')
// And the synchronous phase-A entry point is untouched by any of this.
const sync = recallBlocks(store, '4324', { topK: 1 })
assert.equal(typeof sync?.then, 'undefined', 'recallBlocks must stay synchronous')
} finally {
store.close()
}
})
// ── W4–W7: write-path hardening ───────────────────────────────────────────
//
// The commit path became async, which means it now has a failure surface it did
// not have before: it can be cancelled, it can be slow, and it can be handed a
// vector that does not match the space. Each of those has a required behaviour,
// and each is pinned by a test whose mutation is a one-line revert.
/** A minimal app with the services the plugin injects. */
async function makeMem(config) {
const { apply: applyMemory } = await import('../lib/index.js')
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'w-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
const mem = applyMemory(ctx, { path: join(dir, 'm.db'), ...config })
return {
ctx,
mem,
dir,
async done() {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
},
}
}
/** Invoke the tool with an explicit abort signal. */
function runCommit(ctx, args, signal) {
return ctx.tools.execute({ callId: 'w', name: 'stablemem_commit', arguments: args, signal })
}
// W4 — a wrong-dimension vector must not be stored.
test('W4: a wrong-length vector is treated as an embed failure, not stored', async () => {
const failing = {
// Declares 3, returns 2 — the shape a misconfigured provider produces.
async embedText() {
return [0.1, 0.2]
},
info: () => ({ fingerprint: 'fp-w4', dimension: 3 }),
}
const { ctx, mem, done } = await makeMem({ embedder: failing })
try {
const result = await runCommit(ctx, { text: '维度不对的块 4324' }, new AbortController().signal)
assert.equal(result.isError, false, `commit must not fail: ${JSON.stringify(result.value)}`)
const rows = mem.store.allBlocks(10)
assert.equal(rows.length, 1, 'the block must still land')
assert.equal(rows[0].vector, '', 'and must carry NO vector rather than a bad one')
assert.equal(rows[0].fingerprint, '', 'nor a fingerprint claiming a space it cannot join')
} finally {
await done()
}
})
test('W4†: removing the dimension check lets a bad vector through', async () => {
// Demonstrated at the store level: the store accepts any vector it is given,
// so the check can only live in commit(). This asserts the store's tolerance so
// that a future "the store will catch it" assumption fails here.
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'x', vector: [1, 2], fingerprint: 'fp' })
assert.equal(JSON.parse(store.getBlock(store.allBlocks(1)[0].id).vector).length, 2,
'the store does NOT enforce dimension — commit() must')
} finally {
store.close()
}
})
// W5 — cancellation must not turn into "block not written".
test('W5: an aborted embed still writes the block, without a vector', async () => {
const slow = {
async embedText(_text, signal) {
// A provider that honours abort. If the caller does not pass a signal, this
// hangs until the adapter's 30s timeout — which is the bug W5 guards.
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve([1, 2, 3]), 5000)
signal?.addEventListener('abort', () => {
clearTimeout(timer)
reject(new Error('aborted'))
})
})
},
info: () => ({ fingerprint: 'fp-w5', dimension: 3 }),
}
const { ctx, mem, done } = await makeMem({ embedder: slow })
try {
const controller = new AbortController()
const started = Date.now()
setTimeout(() => controller.abort(), 50)
const result = await runCommit(ctx, { text: '被取消的块 4324' }, controller.signal)
const elapsed = Date.now() - started
assert.ok(elapsed < 2000, `must not wait for the provider/settle after abort; took ${elapsed}ms`)
// ★ The registry owns the final word on an aborted call: after a body returns
// it re-checks the signal and replaces a success with a canonical abort result
// (dsh-tools/lib/index.js:3408 -> toolAbortedResult). So `isError` is NOT the
// assertion here — and asserting `false` would be asserting against the
// harness. What this plugin controls is the SIDE EFFECT: the block is written.
assert.equal(result.isError, true, 'the registry reports the call as aborted')
assert.equal(result.error?.info?.code, 'ABORTED', 'and uses the canonical abort code')
const rows = mem.store.allBlocks(10)
assert.equal(rows.length, 1, 'the block must still be written')
assert.equal(rows[0].vector, '', 'with no vector')
assert.notEqual(rows[0].text_content, '', 'and with its text intact')
} finally {
await done()
}
})
test('W5†: ignoring exec.signal makes the call wait for the provider', async () => {
// The mutation is "execute(args) with no signal plumbing". Emulated by calling
// the embedder without a signal: it runs to completion, which is what the bug
// looked like (up to the adapter's 30 s timeout in production).
const slow = {
async embedText(_text, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve([1, 2, 3]), 300)
signal?.addEventListener('abort', () => {
clearTimeout(timer)
reject(new Error('aborted'))
})
})
},
info: () => ({ fingerprint: 'fp', dimension: 3 }),
}
const started = Date.now()
await slow.embedText('x', undefined) // no signal ⇒ cannot be cancelled
const elapsed = Date.now() - started
assert.ok(elapsed >= 250, `without a signal the call is uncancellable; took ${elapsed}ms`)
})
// W6 — the async wrapper must not change the write result.
test('W6: with no embedder, commit returns the same shape as putBlock', async () => {
const { mem, done } = await makeMem({})
try {
const result = await mem.commit({ text: 'W6 块 7788' })
assert.equal(typeof result.id, 'string')
assert.match(result.id, /^blk_[0-9a-f]{12}$/)
assert.equal(result.created, true)
assert.equal(result.embedded, false)
// Immediately recallable — the write really happened.
const hits = mem.recall('7788')
assert.ok((await hits).length >= 1, 'the block must be findable right after commit')
// Idempotent re-commit reports created=false, exactly like putBlock.
const again = await mem.commit({ text: 'W6 块 7788' })
assert.equal(again.created, false)
assert.equal(again.id, result.id)
} finally {
await done()
}
})
// W7 — an empty fingerprint must degrade BEFORE spending a network call.
test('W7: an empty fingerprint means zero embed calls, not one wasted round-trip', async () => {
let calls = 0
const nameless = {
async embedText() {
calls += 1
return [1, 2, 3]
},
info: () => ({ fingerprint: '', dimension: 3 }),
}
const { mem, done } = await makeMem({ embedder: nameless })
try {
const result = await mem.commit({ text: 'W7 块' })
assert.equal(calls, 0, `an unnamed space must not be asked for a vector; saw ${calls} call(s)`)
assert.equal(result.embedded, false)
const row = mem.store.allBlocks(1)[0]
assert.equal(row.vector, '')
assert.equal(row.fingerprint, '')
} finally {
await done()
}
})
// ── W8: something must actually BUILD the centroid ────────────────────────
//
// `saveSpaceCentroid` had no caller in `lib/`, so in real use blocks carried
// vectors while `graph_centroid` stayed empty forever: `loadCentroid` always
// missed, centering never applied, and the票③ "unknown dimension" state became
// the permanent normal case. The entry point is `rebuildCentroid`, ported from
// HA's `RebuildCentroid` (centroid_store.go:246-320).
test('W8: rebuildCentroid derives the centroid from the blocks of one space', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const fp = 'http:m:3:deadbeef'
// Two near-identical directions plus one different: anisotropic enough to
// cross the 0.6 threshold.
const vectors = [
[1, 0.02, 0.01],
[1, 0.01, 0.02],
[0.99, 0.03, 0.02],
]
for (const [i, v] of vectors.entries()) {
store.putBlock({ text: `块${i}`, vector: v, fingerprint: fp })
}
// A second space must not contaminate the estimate.
store.putBlock({ text: '别空间', vector: [0, 1, 0], fingerprint: 'other:3:ffff' })
// And a mismatched-dimension row in the SAME space must be skipped, not coerced.
store.putBlock({ text: '坏维度', vector: [1, 2], fingerprint: fp })
const result = store.rebuildCentroid(fp)
assert.equal(result.dim, 3, 'dimension comes from the rows, not from configuration')
assert.equal(result.vectorCount, 3, 'only same-space, same-dimension rows count')
assert.ok(result.meanLength > 0)
const saved = store.loadCentroid(fp)
assert.ok(saved, 'the centroid must be persisted')
assert.equal(saved.dim, 3)
assert.equal(saved.vectorCount, 3)
assert.equal(saved.isAnomalous, result.isAnomalous, 'the stored flag matches the verdict')
// The other space must be untouched.
assert.equal(store.loadCentroid('other:3:ffff'), undefined)
} finally {
store.close()
}
})
test('W8: a space with no embedded blocks refuses rather than inventing a centroid', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
assert.throws(
() => store.rebuildCentroid('http:none:3:00000000'),
/no embedded blocks/,
'an empty space has nothing to estimate from',
)
assert.equal(store.loadCentroid('http:none:3:00000000'), undefined)
} finally {
store.close()
}
})
test('W8: the anisotropy verdict uses the 0.6 threshold', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const fp = 'http:m:2:aniso'
// A cone of ±35° gives mean length ~0.879 — anomalous at 0.6, but NOT at
// 0.95. Perfectly collinear vectors (mean length 1.0) would pass any
// threshold, which is how the first version of this test failed to
// distinguish the mutation.
const rad = (35 * Math.PI) / 180
const cone = [[1, 0], [Math.cos(rad), Math.sin(rad)], [Math.cos(-rad), Math.sin(-rad)]]
for (const [i, v] of cone.entries()) {
store.putBlock({ text: `a${i}`, vector: v, fingerprint: fp })
}
const verdict = store.rebuildCentroid(fp)
assert.ok(
verdict.meanLength > 0.6 && verdict.meanLength < 0.95,
`test corpus must sit BETWEEN the thresholds to discriminate; got ${verdict.meanLength.toFixed(4)}`,
)
assert.equal(verdict.isAnomalous, true, 'a ±35° cone is anomalous at the 0.6 threshold')
store.close()
const iso = new BlockStore({ path: ':memory:' }).open()
try {
const fp2 = 'http:m:2:iso'
// Orthogonal directions ⇒ mean length ~0 ⇒ isotropic ⇒ do NOT centre.
for (const [i, v] of [[1, 0], [0, 1], [-1, 0], [0, -1]].entries()) {
iso.putBlock({ text: `i${i}`, vector: v, fingerprint: fp2 })
}
assert.equal(iso.rebuildCentroid(fp2).isAnomalous, false, 'an isotropic space must not be centred')
} finally {
iso.close()
}
} finally {
// outer store already closed above
}
})
// ── W9: the REAL adapter must honour the caller's signal ──────────────────
//
// ★ Why this test exists (the bug it caught): W5/W5† used a hand-written slow
// embedder that registered its own `abort` listener. The real adapter's
// `embedText` took ONE parameter, so `embedText(text, signal)` silently dropped
// the signal and every other test still passed — 99 green while a cancelled turn
// blocked for the adapter's full 30 s and then wrote the vector anyway.
//
// This is the same shape as the V3b "2-message batch is a fake batch" lesson,
// one level up: a test double was standing in for the only code path that could
// be wrong. So W9 uses a real HTTP server and the real `createHttpEmbedder`.
import { createServer } from 'node:http'
/**
* Start a real HTTP endpoint that replies after `delayMs`.
* @param {number} delayMs
* @returns {Promise<{url: string, close: () => Promise<void>}>}
*/
async function slowEndpoint(delayMs) {
const server = createServer((_req, res) => {
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ embedding: [0.1, 0.2, 0.3] }))
}, delayMs)
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()
return {
url: `http://127.0.0.1:${port}/embed`,
close: () => new Promise((resolve) => server.close(resolve)),
}
}
test('W9: a caller abort interrupts a REAL http embedding call promptly', async () => {
const endpoint = await slowEndpoint(3000)
try {
const embedder = createHttpEmbedder({
endpoint: endpoint.url,
dimension: 3,
timeoutMs: 30_000, // deliberately long: only the caller can stop this
})
const controller = new AbortController()
const started = Date.now()
setTimeout(() => controller.abort(), 50)
let threw = false
try {
await embedder.embedText('值', controller.signal)
} catch {
threw = true
}
const elapsed = Date.now() - started
assert.equal(threw, true, 'an aborted call must reject, not resolve with a vector')
assert.ok(
elapsed < 500,
`caller abort must interrupt the request, not wait for it (took ${elapsed}ms)`,
)
} finally {
await endpoint.close()
}
})
test('W9b: the adapter keeps its OWN timeout when a caller signal is present', async () => {
// The other half of the contract: fusing the signals must not disable the
// adapter's own bound. A wedged provider with a live (never-aborted) caller
// signal must still time out.
const endpoint = await slowEndpoint(3000)
try {
const embedder = createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 150 })
const controller = new AbortController() // never aborted
const started = Date.now()
let threw = false
try {
await embedder.embedText('值', controller.signal)
} catch {
threw = true
}
const elapsed = Date.now() - started
assert.equal(threw, true, 'the adapter timeout must still fire')
assert.ok(elapsed < 1000, `own timeout must bound the call (took ${elapsed}ms)`)
} finally {
await endpoint.close()
}
})
test('W9c: end-to-end — aborted tool call still writes the block, vector empty', async () => {
// W5 asserted the side effect through a fake embedder; this does it through
// the real adapter, a real server, and the real tool pipeline.
const endpoint = await slowEndpoint(3000)
try {
const { ctx, mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 30_000 }),
})
try {
const controller = new AbortController()
setTimeout(() => controller.abort(), 50)
const result = await runCommit(ctx, { text: 'W9c 被取消但已落盘 4324' }, controller.signal)
// The registry owns the verdict on an aborted call (dsh-tools:3408).
assert.equal(result.isError, true)
assert.equal(result.error?.info?.code, 'ABORTED')
// What this plugin controls: the side effect.
const rows = mem.store.allBlocks(10)
assert.equal(rows.length, 1, 'the block must be written despite the abort')
assert.equal(rows[0].vector, '', 'without a vector')
assert.ok(rows[0].text_content.includes('4324'), 'and with its text intact')
} finally {
await done()
}
} finally {
await endpoint.close()
}
})
// ── W10: the READ path must honour cancellation too ───────────────────────
//
// W9 fixed the write path. The read path had the same defect and nobody had
// looked: `recallBlocksHybrid` called `embedText(text)` with ONE argument, and
// neither `StableMem.recall()` nor `contextForStep()` accepted a signal — even
// though the `agent/pre-step` payload carries one
// (dsh-agent/lib/types/runtime-types.d.ts:304-310). So every pre-step could block
// for the adapter's full 30 s timeout while the user had already cancelled.
//
// This test goes through the REAL hook chain (pre-step → contextForStep → recall
// → adapter → http), not by calling recallBlocksHybrid directly, because that is
// the path a cancelled turn actually takes.
/** A server that accepts the request and never answers. */
async function hangingEndpoint() {
const server = createServer((req) => {
req.on('data', () => {})
req.on('end', () => {})
// deliberately never respond
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()
return {
url: `http://127.0.0.1:${port}/embed`,
close: () =>
new Promise((resolve) => {
server.closeAllConnections?.()
server.close(resolve)
}),
}
}
test('W10: aborting a pre-step interrupts the recall embedding promptly', async () => {
const endpoint = await hangingEndpoint()
try {
const { ctx, mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 30_000 }),
})
try {
// One embedded block so the vector path is actually taken.
await mem.commit({ text: 'x', vector: [1, 2, 3], fingerprint: mem.space().fingerprint })
const batch = [
createUserMessage({
content: [{ type: 'text', text: '值班室分机号是多少' }],
source: { kind: 'user' },
}),
]
const controller = new AbortController()
const started = Date.now()
setTimeout(() => controller.abort(), 50)
const decision = await ctx.waterfall(
'agent/pre-step',
{ agent: { id: 'a', session: {} }, messages: batch, turn: 1, step: 1, signal: controller.signal },
() => Promise.resolve({ kind: 'enter', messages: batch }),
)
const elapsed = Date.now() - started
assert.ok(
elapsed < 500,
`an aborted pre-step must not wait for the embedding provider; took ${elapsed}ms`,
)
// It must still produce a decision (recall degrades, the turn continues).
assert.ok(decision.kind === 'enter' || decision.kind === 'reject')
// And the degradation must be reported, not silently swallowed.
const hits = await mem.recall('值班室分机号是多少')
assert.equal(hits.degraded, 'embed_error', 'an aborted embed is a degraded recall')
} finally {
await done()
}
} finally {
await endpoint.close()
}
})
test('W10b: a signal reaching recallBlocksHybrid directly also aborts', async () => {
// The unit-level twin of W10: proves the parameter is honoured at the seam, so
// a future refactor of the hook chain cannot hide a regression here.
const endpoint = await hangingEndpoint()
try {
const embedder = createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 30_000 })
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324' })
const controller = new AbortController()
const started = Date.now()
setTimeout(() => controller.abort(), 50)
const hits = await recallBlocksHybrid(store, embedder, '4324', {
topK: 3,
signal: controller.signal,
})
const elapsed = Date.now() - started
assert.ok(elapsed < 500, `must abort promptly; took ${elapsed}ms`)
assert.equal(hits.degraded, 'aborted', 'a cancellation is not a provider failure')
assert.ok(hits.length >= 1, 'and still answer from the symbol path')
} finally {
store.close()
}
} finally {
await endpoint.close()
}
})
// ── W11: degradation must be READABLE, not merely held on an object ───────
//
// `hits.degraded` is deliberately non-enumerable so it cannot disturb the
// phase-A golden comparison. The side effect is that it is invisible to
// JSON.stringify, Object.keys, and any serialised log — so a test asserting the
// property proves nothing about what the model receives. The only channel the
// model has is the rendered text.
test('W11: a degraded recall puts a readable notice in the rendered prompt text', () => {
const degraded = [{ text: '值班室分机号 4324', exact: true, score: 1.01 }]
degraded.degraded = 'embed_error'
const rendered = renderRecall(degraded, 'dsh-memory')
assert.ok(
rendered.includes('FAILED'),
`the model must be told the search was partial; rendered:\n${rendered}`,
)
assert.ok(
rendered.includes('does not prove it is unrecorded'),
'and must be warned that absence of evidence is not evidence of absence',
)
// Sanity: the property really is invisible to serialisation, which is WHY the
// text channel has to exist.
assert.equal(JSON.stringify(degraded).includes('degraded'), false)
})
test('W11b: a healthy recall renders no notice at all', () => {
const healthy = [{ text: '值班室分机号 4324', exact: true, score: 1.01 }]
const rendered = renderRecall(healthy, 'dsh-memory')
assert.ok(!rendered.includes('FAILED'), 'no false alarm on a healthy recall')
assert.ok(!rendered.includes('Semantic search is off'))
assert.ok(rendered.includes('PAST notes'), 'the header is still there')
})
test('W11c: an aborted pre-step renders the notice end to end', async () => {
// The full chain: abort → embed_error → notice in the text the model receives.
// For the notice to appear there must also be at least one hit, so seed a block
// the *symbol* path can still find — recall degrades to lexical, it does not
// return nothing.
const endpoint = await hangingEndpoint()
try {
const { ctx, mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 30_000 }),
})
try {
await mem.commit({ text: 'x', vector: [1, 2, 3], fingerprint: mem.space().fingerprint })
await mem.commit({ text: '值班室分机号 4324' })
const controller = new AbortController()
const started = Date.now()
setTimeout(() => controller.abort(), 50)
const text = await mem.contextForStep(
[
createUserMessage({
content: [{ type: 'text', text: '值班室分机号 4324' }],
source: { kind: 'user' },
}),
],
controller.signal,
)
const elapsed = Date.now() - started
assert.ok(elapsed < 500, `the aborted recall must not hang; took ${elapsed}ms`)
assert.notEqual(text, '', 'the symbol path still produced content')
assert.ok(
/cancelled|FAILED/.test(text),
`the notice must reach the model-facing text; got:\n${text}`,
)
assert.ok(text.includes('cancelled'), 'a cancellation says so, not "FAILED"')
assert.ok(text.includes('4324'), 'and the hit itself is present')
} finally {
await done()
}
} finally {
await endpoint.close()
}
})
test('W11†: removing the notice line makes the degradation invisible again', () => {
// The mutation is "renderRecall goes back to header + lines". Emulated by
// building the old shape, and asserting it does NOT contain the warning.
const degraded = [{ text: 'x', exact: true, score: 1 }]
degraded.degraded = 'embed_error'
const oldShape = [
'[Memory · dsh-memory] Recalled from durable block memory — these are PAST notes, not the current conversation.',
...degraded.map((h, i) => `[${i + 1}] (${h.exact ? 'exact' : 'text'} ${h.score.toFixed(2)}) ${h.text}`),
].join('\n')
assert.ok(!oldShape.includes('FAILED'), 'the old shape carries no warning — so W11 would fail on it')
assert.ok(renderRecall(degraded, 'dsh-memory').includes('FAILED'), 'the current shape does')
})
// ── W11d: degraded AND empty must still say something ────────────────────
//
// The mixed case: recall was degraded (so absence of evidence is not evidence of
// absence) and the lexical fallback found nothing (so there are no entries). The
// model must be told the search was incomplete — staying silent here is exactly
// the false negative the notice exists to prevent.
//
// ★ Two short-circuits suppress it, and fixing only one changes nothing:
// 1. `contextForStep`: `if (hits.length === 0) return ''`
// 2. `renderRecall`: `if (hits.length === 0) return ''`
// Both must treat "no entries" and "nothing to say" as different questions. Each
// gets its own mutation below, because fixing one and reverting the other is
// invisible to a test that only exercises the fixed path.
//
// Measured behaviour of the four combinations (real adapter, real hooks):
// A degraded + hits -> text, FAILED, entries
// B degraded + no hits -> text, FAILED, NO entries ← this test
// C1 healthy + hits -> text, no FAILED, entries
// C2 healthy + no hits -> EMPTY (correct silence)
test('W11d: degraded + zero hits still injects the notice, with no entries', async () => {
const endpoint = await hangingEndpoint()
try {
const { mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 30_000 }),
})
try {
// One embedded block so the vector path is taken, but its text shares no
// symbol with the query, so the lexical fallback finds nothing.
await mem.commit({ text: 'zzz 完全无关的内容', vector: [1, 2, 3], fingerprint: mem.space().fingerprint })
const controller = new AbortController()
setTimeout(() => controller.abort(), 50)
const text = await mem.contextForStep(
[
createUserMessage({
content: [{ type: 'text', text: '值班室分机号是多少' }],
source: { kind: 'user' },
}),
],
controller.signal,
)
assert.notEqual(text, '', 'a degraded empty recall must not be silent')
assert.ok(
/cancelled|FAILED/.test(text),
`the notice must be present; got:\n${text}`,
)
assert.ok(
!text.includes('[1]'),
`and it must carry no entries; got:\n${text}`,
)
} finally {
await done()
}
} finally {
await endpoint.close()
}
})
// NOTE: two tests formerly named W11d†a / W11d†b lived here. Each built its own
// stand-in function inside the test file and asserted on that — they touched no
// `lib/` code, so they stayed GREEN under both real mutations while W11d itself
// went red. They were self-tests masquerading as mutation guards, which is the
// same defect as the G5 allowlist that was silently always-false: a guard that
// cannot fail is not a guard. They have been removed rather than kept as count.
//
// The real guards are:
// · W11d — asserts the behaviour; goes red under either mutation.
// · W11d† — performs the renderRecall mutation on `lib/` and asserts the
// behaviour flips (above, in the mutation-runner section).
// · W11d†ctx — does the same for the contextForStep short-circuit (below).
//
// A mutation must change a file under `lib/`; if it edits `test/`, it is not a
// mutation. That rule is now written down in README so the next one is caught.
test('W11d†ctx: restoring the contextForStep short-circuit flips the behaviour', async () => {
// This is the second of the two independent short-circuits. It needs its own
// mutation because fixing renderRecall alone leaves the caller discarding the
// notice before it is ever rendered.
await withMutatedCopy(
'index.js',
[
[
' const hits = await this.recall(query, { signal })',
" const hits = await this.recall(query, { signal })\n if (hits.length === 0) return '' // MUTATED: the old caller short-circuit",
],
],
async (mod) => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'w11dctx-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = mod.apply(ctx, { path: join(dir, 'm.db') })
const hits = []
hits.degraded = 'embed_error'
// Drive the mutated method with a degraded-empty recall, so the caller
// short-circuit is the only thing that can decide the outcome.
mem.recall = async () => hits
const text = await mem.contextForStep([
createUserMessage({ content: [{ type: 'text', text: '任意问题' }], source: { kind: 'user' } }),
])
assert.equal(
text,
'',
'under the mutation the caller returns "" for a degraded-empty recall, so W11d must fail',
)
// Prove the notice would have existed: import the (unmutated) renderer.
const { renderRecall } = await import('../lib/recall.js')
assert.notEqual(
renderRecall(hits, 'x'),
'',
'renderRecall would still have produced the notice — proving the caller dropped it',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
// ── W12: cancellation and failure must not share a label ─────────────────
//
// Both used to report `embed_error`, so a user-cancelled turn told the model
// "semantic search FAILED", which invites a retry that will also be cancelled.
// They are different facts: `aborted` is a deliberate stop, `embed_error` is a
// provider fault where absence of evidence really is suspicious.
test('W12: an aborted embed reports "aborted"; a failing one reports "embed_error"', async () => {
const endpoint = await hangingEndpoint()
try {
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324' })
const embedder = createHttpEmbedder({ endpoint: endpoint.url, dimension: 3, timeoutMs: 30_000 })
// Cancelled.
const controller = new AbortController()
setTimeout(() => controller.abort(), 50)
const aborted = await recallBlocksHybrid(store, embedder, '4324', {
topK: 3,
signal: controller.signal,
})
assert.equal(aborted.degraded, 'aborted')
// Genuinely failing: a server that closes the socket immediately.
const broken = {
async embedText() {
throw new Error('connection refused')
},
info: () => ({ fingerprint: 'fp', dimension: 3 }),
}
const failed = await recallBlocksHybrid(store, broken, '4324', { topK: 3 })
assert.equal(failed.degraded, 'embed_error')
// The two labels must read differently to the model.
assert.notEqual(
degradationNotice(aborted),
degradationNotice(failed),
'a cancellation must not be described as a failure',
)
assert.match(degradationNotice(aborted), /cancel/i)
assert.match(degradationNotice(failed), /FAILED/)
} finally {
store.close()
}
} finally {
await endpoint.close()
}
})
test('W12†: collapsing the two labels makes W12 red', () => {
// The mutation is `aborted ? 'aborted' : 'embed_error'` -> always 'embed_error'.
const hits = []
hits.degraded = 'embed_error' // what the collapsed version would report
assert.doesNotMatch(
degradationNotice(hits),
/cancel/i,
'the collapsed version cannot say "cancelled", which is what W12 asserts',
)
})
// ── W11e: a permanent configuration fact must not be repeated every turn ──
//
// W11d made "degraded + zero hits" speak up. That is right for a per-turn event
// (an embed failure, a cancellation) and wrong for a standing configuration fact.
// Without an embedder is the **shipping default**, so `no_embedder` is not an
// event at all — and `renderIndex()` (systemPrompt.context) already tells the
// model every turn that block memory exists and is searched lexically.
//
// Measured before the fix, real hooks, phase-A default config, 10 ordinary turns:
// 10 injected, 2130 chars, mean 213 chars/turn
// The cost is not the tokens — it is that a constant, entry-less memory block on
// every turn teaches the model to skip the section, which is exactly what W11
// exists to prevent for real degradations.
//
// W11d and W11e are therefore mutual guards:
// per-turn event ⇒ speak (W11d)
// standing state ⇒ stay silent (W11e)
test('W11e: with the default config (no embedder), a zero-hit turn injects nothing', async () => {
const { mem, done } = await makeMem({}) // phase-A shipping default: no embedder
try {
await mem.commit({ text: '某条无关记忆' })
const ordinaryTurns = ['今天天气怎么样', '你好', '谢谢', '帮我看看这个', '早上好',
'嗯', '在吗', '收到', '好的', '再见']
const injected = []
for (const turn of ordinaryTurns) {
const text = await mem.contextForStep([
createUserMessage({ content: [{ type: 'text', text: turn }], source: { kind: 'user' } }),
])
if (text !== '') injected.push({ turn, text })
}
assert.deepEqual(
injected,
[],
`${injected.length}/${ordinaryTurns.length} ordinary turns were injected on the default config; ` +
`first: ${JSON.stringify(injected[0]?.text?.slice(0, 120))}`,
)
} finally {
await done()
}
})
// ── W11e† and W11d†: real mutations, performed on lib/ by the runner ──────
//
// ★ Earlier versions of these "mutation" tests built a stand-in function inside
// the test file and asserted on it. They touched no lib/ code, so they stayed
// green under the real mutations — they were self-tests, not guards. A mutation
// must change `lib/` or it is not a mutation.
//
// The runner below performs each mutation by actually patching the source,
// importing it fresh, and asserting the behaviour flips. If a patch cannot be
// applied (because the source moved), the test fails loudly rather than silently
// passing.
/**
* Run `body` against a MUTATED COPY of one lib module.
*
* ★ Why a copy, not an in-place patch: `node --test` runs test FILES in parallel
* processes. An earlier version rewrote `lib/recall.js` on disk, imported it, and
* restored it — which raced every other file importing the same module at the
* same moment. It produced a ~1-in-30 spurious failure (observed: a run where
* W11d† saw the unmutated module) that looked like flakiness in the assertion
* rather than a bug in the harness. A mutation of shared state is not safe to run
* concurrently with the code under test.
*
* The copy preserves directory shape, so relative imports (`./recall.js`) still
* resolve to mutated siblings if they exist, and everything else resolves through
* the real `node_modules`.
*
* @param {string} relPath module to mutate, relative to lib/ (e.g. 'recall.js')
* @param {Array<[string, string]>} replacements exact-text substitutions
* @param {(mod: object, dir: string) => Promise<void>} body
*/
async function withMutatedCopy(relPath, replacements, body) {
const { readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync, cpSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join, dirname } = await import('node:path')
const { fileURLToPath, pathToFileURL } = await import('node:url')
const libDir = fileURLToPath(new URL('../lib/', import.meta.url))
const pkgRoot = fileURLToPath(new URL('..', import.meta.url))
const dir = mkdtempSync(join(tmpdir(), 'mut-'))
try {
// Copy lib/ wholesale, plus a package.json marking ESM, so imports work.
cpSync(libDir, join(dir, 'lib'), { recursive: true })
cpSync(join(pkgRoot, 'package.json'), join(dir, 'package.json'))
// `node_modules` is needed for the @deepseek-ai peers; link rather than copy.
try {
cpSync(join(pkgRoot, 'node_modules'), join(dir, 'node_modules'), { recursive: true, dereference: false })
} catch {
// A bare install without node_modules simply cannot resolve peers; the
// caller's body will fail loudly, which is the correct outcome.
}
const target = join(dir, 'lib', relPath)
let text = readFileSync(target, 'utf8')
for (const [from, to] of replacements) {
assert.ok(
text.includes(from),
`mutation anchor not found in lib/${relPath} — did the source move?\n${from.slice(0, 100)}`,
)
text = text.replace(from, to)
}
writeFileSync(target, text)
const mod = await import(pathToFileURL(target).href)
return await body(mod, dir)
} finally {
rmSync(dir, { recursive: true, force: true })
}
}
test('W11e†: making no_embedder speak again turns W11e red', async () => {
await withMutatedCopy(
'recall.js',
[
[
` case 'no_embedder':
// Permanent configuration state, not a per-turn failure.`,
` case 'no_embedder':
return 'MUTATED: semantic search is off'
// Permanent configuration state, not a per-turn failure.`,
],
],
async (mod) => {
const hits = []
hits.degraded = 'no_embedder'
assert.notEqual(
mod.degradationNotice(hits),
'',
'under the mutation the notice is non-empty, so W11e (which asserts "") must fail',
)
},
)
})
test('W11d†: restoring the renderRecall short-circuit turns W11d red', async () => {
await withMutatedCopy(
'recall.js',
[
[
' if (lines.length === 0 && notice === \'\') return \'\'',
' if (lines.length === 0) return \'\' // MUTATED: the old short-circuit',
],
],
async (mod) => {
const hits = []
hits.degraded = 'embed_error'
assert.equal(
mod.renderRecall(hits, 'x'),
'',
'under the mutation a degraded-but-empty recall renders nothing, so W11d must fail',
)
},
)
})
// ── W11f: the notice must never promise entries it does not have ──────────
//
// Two wordings exist per degradation mode because "matches below are …" is a lie
// when the lexical fallback found nothing. `embed_error` got that split first;
// `aborted` and the `default:` fallback said it unconditionally.
//
// ★ The mode list is DERIVED FROM THE SOURCE, not hardcoded. A hardcoded list
// silently stops covering the codebase the moment someone adds a `case`:
// measured — planting `case 'brand_new_mode': return '… matches below …'` left a
// hardcoded version fully green, because the new mode was simply not in the
// array. Scanning `case '<literal>'` inside `degradationNotice` makes every
// future branch fall under the contract automatically, which is the whole point
// of putting the obligation on a shared `nothing` flag.
test('W11f: no mode promises entries below when there are none, and all point at them when there are', async () => {
const { readFileSync } = await import('node:fs')
const src = readFileSync(new URL('../lib/recall.js', import.meta.url), 'utf8')
const body = src.slice(src.indexOf('export function degradationNotice('))
const declared = [...body.slice(0, body.indexOf('\n}\n')).matchAll(/case '([^']+)':/g)]
.map((m) => m[1])
// A sanity floor: if the extraction breaks (function renamed, cases moved
// behind a helper), the test must fail loudly rather than iterate nothing.
assert.ok(
declared.length >= 4,
`expected at least 4 case literals in degradationNotice, found ${declared.length}: ${declared.join(', ')}`,
)
for (const required of ['no_embedder', 'embed_error', 'aborted']) {
assert.ok(declared.includes(required), `extraction missed ${required} — did the shape change?`)
}
// Every declared mode, plus the two states no `case` can name: "no degradation
// at all", and a mode that has no branch (falls through to `default:`).
const MODES = [undefined, ...declared, 'mode_with_no_branch_at_all']
for (const mode of MODES) {
const empty = []
if (mode !== undefined) empty.degraded = mode
const noticeEmpty = degradationNotice(empty)
if (noticeEmpty !== '') {
assert.ok(
!/matches below|entries below|below are/i.test(noticeEmpty),
`${mode} + zero hits must not promise entries below; got: ${noticeEmpty}`,
)
}
const withHit = [{ text: '值班室分机号 4324', exact: true, score: 1.01 }]
if (mode !== undefined) withHit.degraded = mode
const noticeHit = degradationNotice(withHit)
if (noticeHit !== '') {
assert.ok(
/matches below|below are|entry|entries/i.test(noticeHit),
`${mode} + one hit must point at the entries; got: ${noticeHit}`,
)
assert.ok(
!/found nothing/i.test(noticeHit),
`${mode} + one hit must not claim nothing was found; got: ${noticeHit}`,
)
}
}
})
test('W11f-a: the scan actually sees a newly added case (guards the guard)', async () => {
// The extraction above is the one thing that can silently disarm this suite:
// if it stopped finding cases, the loop would iterate almost nothing and stay
// green. Prove it sees a case that did not exist when this test was written.
await withMutatedCopy(
'recall.js',
[[
` case 'vector_ready_unused':`,
` case 'mode_added_by_the_future_test':
return 'PROBE'
case 'vector_ready_unused':`,
]],
async (mod) => {
const hits = []
hits.degraded = 'mode_added_by_the_future_test'
assert.equal(mod.degradationNotice(hits), 'PROBE', 'the new branch is reachable')
},
)
})
test('W11f†: dropping the aborted split makes W11f red', async () => {
await withMutatedCopy(
'recall.js',
[[
` return nothing
? '[Memory] Semantic search was cancelled for this turn and the lexical fallback found nothing.'
: '[Memory] Semantic search was cancelled for this turn; matches below are lexical/symbolic only.'`,
` return '[Memory] Semantic search was cancelled for this turn; matches below are lexical/symbolic only.'`,
]],
async (mod) => {
const empty = []
empty.degraded = 'aborted'
assert.match(
mod.degradationNotice(empty),
/matches below/i,
'the mutation restores unconditional wording, so W11f must go red on aborted+zero',
)
},
)
})
test('W11f†b: dropping the default-branch split makes W11f red', async () => {
await withMutatedCopy(
'recall.js',
[[
` return nothing
? \`[Memory] Recall degraded (\${mode}); nothing was found, so treat this as "unknown", not "absent".\`
: \`[Memory] Recall degraded (\${mode}); matches below may be incomplete.\``,
` return \`[Memory] Recall degraded (\${mode}); matches below may be incomplete.\``,
]],
async (mod) => {
const empty = []
empty.degraded = 'future_mode_a'
assert.match(
mod.degradationNotice(empty),
/matches below/i,
'without the split the fallback lies again, so W11f must go red',
)
},
)
})
// ── F2: the space filter must cover the SYMBOL path, not just the vector path ──
//
// HA shipped with the vector path filtering by fingerprint and the symbol path
// not filtering at all. Their own comment records the outcome as "一个真实的
// 安全缺陷" (internal/memory/fusion.go:481-495): text matching does not depend on
// the vector space, so after any provider change, blocks from the dead space kept
// showing up in symbol results.
//
// ★ The tests below deliberately exercise the path where NO embedding happens —
// `recallBlocks` called with a `fingerprint` but the symbol machinery doing the
// retrieval. A suite that only exercised `recallBlocksHybrid` with a live embedder
// would pass while the symbol path leaked, which is exactly HA's bug shape.
/** Seed three blocks: current space, stale space, and no vector at all. */
function seedSpaces(store) {
store.putBlock({ text: '值班室分机号 4324 当前空间', vector: [1, 0, 0], fingerprint: 'fp-current' })
store.putBlock({ text: '值班室分机号 4324 旧空间', vector: [0, 1, 0], fingerprint: 'fp-stale' })
// No vector ⇒ not a vector-path participant; its text must stay searchable.
store.putBlock({ text: '值班室分机号 4324 无向量' })
}
test('F2: a symbol-path hit from a stale space is excluded', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedSpaces(store)
const scoped = recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-current' })
const texts = scoped.map((hit) => hit.text)
assert.equal(texts.length, 2, `expected current + no-vector, got ${JSON.stringify(texts)}`)
assert.ok(
!texts.some((text) => text.includes('旧空间')),
`a stale-space block must not appear in symbol results; got ${JSON.stringify(texts)}`,
)
assert.ok(scoped.every((hit) => hit.fingerprint !== 'fp-stale'), 'and no hit may carry the stale space')
} finally {
store.close()
}
})
test('F2: without a fingerprint argument the filter is inert (phase-A behaviour)', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedSpaces(store)
const unscoped = recallBlocks(store, '4324', { topK: 10 })
assert.equal(
unscoped.length,
3,
'every block matches when no space is declared — the filter must not run unconditionally',
)
} finally {
store.close()
}
})
test('F2b: a block with a vector but NO fingerprint is fail-CLOSED (§0.2)', () => {
// §0.2 ruling: fail-closed, deliberately NOT HA's fail-open. HA's condition is
// `q.fp != "" && b.fp != "" && b.fp != q.fp`, so an empty block fingerprint makes
// the middle term false and the block is PASSED THROUGH. This project excludes
// it instead: a vector whose space cannot be named is comparable to nothing.
//
// This state is only reachable for rows written before the F5 write-side guard
// existed, so it is constructed directly at the store level.
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324 无名向量' })
// Simulate a pre-guard row: give it a vector while leaving the fingerprint empty.
store.raw().prepare("UPDATE memory_blocks SET vector = ? WHERE text_content LIKE '%无名向量%'")
.run(JSON.stringify([1, 2, 3]))
store.putBlock({ text: '值班室分机号 4324 正常', vector: [1, 0, 0], fingerprint: 'fp-current' })
const hits = recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-current' })
const texts = hits.map((hit) => hit.text)
assert.ok(
!texts.some((text) => text.includes('无名向量')),
`an unidentifiable vector must be excluded, not passed through; got ${JSON.stringify(texts)}`,
)
assert.ok(texts.some((text) => text.includes('正常')), 'the current-space block still shows')
} finally {
store.close()
}
})
test('F2: recallBlocksHybrid applies the same filter on every degradation path', async () => {
// The filter must not depend on which branch hybrid takes. With a nameless
// embedder, hybrid degrades to `no_embedder` and STILL must not return a stale
// block — that is the path where an injected-at-call-site fingerprint would be
// easiest to forget.
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedSpaces(store)
const nameless = { async embedText() { return [1, 2, 3] }, info: () => ({ fingerprint: '', dimension: 3 }) }
const hits = await recallBlocksHybrid(store, nameless, '4324', { topK: 10 })
assert.equal(hits.degraded, 'no_embedder')
// A nameless embedder declares no space, so the filter is inert — every block
// is legitimately searchable. The assertion here is that it does not CRASH or
// silently drop everything.
assert.equal(hits.length, 3, 'an undeclared space must not exclude vectorised blocks')
const named = { async embedText() { return [1, 2, 3] }, info: () => ({ fingerprint: 'fp-current', dimension: 3 }) }
const scoped = await recallBlocksHybrid(store, named, '4324', { topK: 10 })
assert.ok(
!scoped.some((hit) => hit.text.includes('旧空间')),
'with a named space, hybrid must filter the stale block even on the symbol path',
)
} finally {
store.close()
}
})
// ── F2 mutations: one per path, because one path leaking is the whole bug ──
//
// Two independent mutations, because the filter can be defeated in two ways:
// 1. drop the filter (recall.js) -> every stale block returns
// 2. stop carrying identity on the containment path (store.js) -> the filter
// sees `hasVector === undefined` and lets that intake path through
// Mutation 2 is the subtle one: it is exactly how a filter that "looks present"
// covers only one of two entry points.
test('F2†: removing the space filter makes the stale block reappear', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
await withMutatedCopy(
'recall.js',
[[
` if (typeof fingerprint === 'string' && fingerprint !== '') {`,
` if (false) { // MUTATED: no space filter`,
]],
async (mod) => {
// The mutated module needs a store; use the real one — the mutation is in
// the filter, not in storage.
const store = new RealStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324 当前', vector: [1, 0, 0], fingerprint: 'fp-current' })
store.putBlock({ text: '值班室分机号 4324 旧空间', vector: [0, 1, 0], fingerprint: 'fp-stale' })
const hits = mod.recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-current' })
const texts = hits.map((h) => h.text)
assert.ok(
texts.some((t) => t.includes('旧空间')),
'under the mutation the stale block returns — so F2 (which forbids it) fails',
)
} finally {
store.close()
}
},
)
})
test('F2†b: dropping identity from the containment path defeats the filter', async () => {
// Mutate the store so containment hits stop carrying `hasVector`/`fingerprint`.
// This is the realistic way a filter goes half-blind: the FTS query gets the new
// columns, the fallback query is forgotten.
const { BlockStore: RealStore } = await import('../lib/store.js')
await withMutatedCopy(
'store.js',
[[
` fingerprint: row.fingerprint,
hasVector: row.has_vector === 1,`,
` // MUTATED: identity dropped on the FTS path`,
]],
async (mod) => {
const store = new mod.BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324 当前', vector: [1, 0, 0], fingerprint: 'fp-current' })
store.putBlock({ text: '值班室分机号 4324 旧空间', vector: [0, 1, 0], fingerprint: 'fp-stale' })
// The real recallBlocks sees `hasVector === undefined` for every FTS hit,
// so the space filter no longer fires.
const { recallBlocks } = await import('../lib/recall.js')
const hits = recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-current' })
assert.ok(
hits.some((h) => h.text.includes('旧空间')),
'without identity on the FTS path the filter is blind — proving F2 depends on it',
)
} finally {
store.close()
}
},
)
})
// ── F6: three layers, one case, all three asserted ────────────────────────
//
// F6 used to read "still retrievable by text, not part of vector recall", which
// parses as a contradiction. Ruling: those are three different layers, and
// "retrievable by text" means a STORE-LEVEL browse with no space argument — not
// the production recall path.
//
// The rewrite is not cosmetic: F2 (the fusion.go:495-class security fix) has
// priority over F6's literal self-consistency, so F6 adapts to F2 rather than the
// other way round.
//
// Why all three in ONE case: asserting only "filtered out" would pass even if the
// rows had been deleted (data loss reads as correct filtering); asserting only
// "still retrievable" would force F2 to be reverted. Both halves are needed, and
// they must be checked against the same store state.
test('F6: after a space change the old block is kept, browsable, and not recalled', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
// A block written under an OLD space, then a block under the current one.
const old = store.putBlock({ text: '值班室分机号 4379 旧空间', vector: [0, 1, 0], fingerprint: 'fp-old' })
store.putBlock({ text: '值班室分机号 4324 当前空间', vector: [1, 0, 0], fingerprint: 'fp-current' })
const blocksBefore = store.countBlocks()
// ── layer 1: DATA — nothing was deleted, both spaces still have rows ──
assert.equal(store.countBlocks(), blocksBefore, 'a space change must not delete rows')
const spaces = store.blockFingerprints()
const byFP = Object.fromEntries(spaces.map((row) => [row.fingerprint, row.blocks]))
assert.equal(byFP['fp-old'], 1, `the old space must still be listed; got ${JSON.stringify(byFP)}`)
assert.equal(byFP['fp-current'], 1)
assert.ok(store.getBlock(old.id), 'the old block is still readable by id')
// ── layer 2: BROWSE — a store-level text query with NO space still finds it ──
const browsed = recallBlocks(store, '4379', { topK: 10 })
assert.ok(
browsed.some((hit) => hit.text.includes('旧空间')),
'a space-free browse must still reach the old block — that is what "retrievable by text" means',
)
// ── layer 3: RECALL — the production path (with a fingerprint) excludes it ──
const recalled = recallBlocks(store, '4379', { topK: 10, fingerprint: 'fp-current' })
assert.ok(
!recalled.some((hit) => hit.text.includes('旧空间')),
'the space-scoped recall path must exclude the old block (F2)',
)
// And the current-space block behaves the same way in both layers, so the
// assertions above are not accidentally passing because *nothing* is found.
assert.ok(recallBlocks(store, '4324', { topK: 10 }).length > 0, 'browse sees the current space too')
assert.ok(
recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-current' })
.some((hit) => hit.text.includes('当前空间')),
'and scoped recall still returns current-space hits',
)
} finally {
store.close()
}
})
/** The injected code: "clean up rows from other spaces" — the data-loss mutation. */
const MUTATION_TEXT = [
" // MUTATED: purge rows belonging to other spaces (data loss disguised as cleanup).",
" if (vectorFP !== '') {",
" db.prepare('DELETE FROM memory_blocks WHERE fingerprint != ? AND fingerprint != ' + String.fromCharCode(39) + String.fromCharCode(39)).run(vectorFP)",
" }",
" // FTS is not auto-maintained; delete-then-insert keeps it in step.",
].join('\n')
test('F6†data: a space change that DELETES the old rows trips the DATA assertion', async () => {
// The realistic wrong implementation: "the space changed, so purge the rows".
// Data loss looks like correct filtering from the recall layer's point of view,
// which is why F6 must assert the data layer separately.
//
// The mutation is applied to the store: `putBlock` gains a purge of rows from
// other spaces, i.e. someone implements de-duplication by deletion.
const { BlockStore: RealStore } = await import('../lib/store.js')
await withMutatedCopy(
'store.js',
[[
` // FTS is not auto-maintained; delete-then-insert keeps it in step.`,
MUTATION_TEXT,
]],
async (mod) => {
const store = new mod.BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'old space block', vector: [1, 0], fingerprint: 'fp-old' })
// Writing into the current space purges the other one under the mutation.
store.putBlock({ text: 'current space block', vector: [0, 1], fingerprint: 'fp-current' })
const byFP = Object.fromEntries(
store.blockFingerprints().map((row) => [row.fingerprint, row.blocks]),
)
assert.equal(
byFP['fp-old'],
undefined,
'the mutation really does destroy the old space — so the DATA assertion fails',
)
assert.equal(store.countBlocks(), 1, 'and the row count dropped, which F6 asserts against')
} finally {
store.close()
}
},
)
})
test('F6†recall: dropping the filter trips the RECALL assertion', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
await withMutatedCopy(
'recall.js',
[[
` if (typeof fingerprint === 'string' && fingerprint !== '') {`,
` if (false) { // MUTATED: no space filter`,
]],
async (mod) => {
const store = new RealStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4379 旧空间', vector: [0, 1, 0], fingerprint: 'fp-old' })
store.putBlock({ text: '值班室分机号 4324 当前空间', vector: [1, 0, 0], fingerprint: 'fp-current' })
const recalled = mod.recallBlocks(store, '4379', { topK: 10, fingerprint: 'fp-current' })
assert.ok(
recalled.some((hit) => hit.text.includes('旧空间')),
'without the filter the old block returns — so the RECALL assertion would fail',
)
} finally {
store.close()
}
},
)
})
// ══════════════════════════════════════════════════════════════════════════
// F3/F4: four-state space accounting
// ══════════════════════════════════════════════════════════════════════════
//
// F2 makes the filter exclude blocks; F3/F4 make the exclusion COUNTABLE. Doing
// the counting first would have produced a permanently-zero metric — which is why
// F2 landed before this.
//
// The four states, and what each one tells an operator to do:
//
// noVector vector is empty ⇒ backfill (embed it)
// unknownSpace has a vector, cannot name its space ⇒ re-embed (unlabelled)
// spaceMismatch has a vector, named, but ≠ active ⇒ re-embed into the active space
// embedFailure this CALL could not get a vector ⇒ retry / check provider
//
// ★ The first three are LIBRARY STATE — derived from rows, so they must be
// computed from the DB on every read (D4). Only `embedFailure` is a per-call
// event, because it describes something that happened, not something stored.
//
// ★ F4 requires dim-mismatch and fingerprint-mismatch to be counted SEPARATELY:
// the remedies differ (change provider config vs re-embed), so collapsing them
// into one number leaves the operator unable to act.
/** Seed one block in each of the four derivable states. */
function seedFourStates(store) {
store.putBlock({ text: 'A 当前空间', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'B 旧空间 缓存', vector: [0, 1, 0], fingerprint: 'fp-old' })
store.putBlock({ text: 'C 无向量' })
store.putBlock({ text: 'D 无名向量 缓存' })
// A pre-F5 row: a vector with no space identity. Only constructible by writing
// around the guard, which is exactly the legacy case §0.2 must still account for.
store.raw().prepare("UPDATE memory_blocks SET vector = ? WHERE text_content = 'D 无名向量 缓存'")
.run(JSON.stringify([0, 0, 1]))
}
test('F3: the four states are derived from the DB, and skipped is non-zero', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedFourStates(store)
const counts = store.blockStates('fp-cur')
assert.deepEqual(
{ noVector: counts.noVector, unknownSpace: counts.unknownSpace, spaceMismatch: counts.spaceMismatch },
{ noVector: 1, unknownSpace: 1, spaceMismatch: 1 },
`exactly one block in each derivable state; got ${JSON.stringify(counts)}`,
)
assert.equal(counts.total, 4)
// F2's filter must actually skip blocks, and the recall result must report
// that — a count that never exceeds zero is the fake-green this ordering
// exists to prevent.
//
// The query is chosen so that BOTH excluded blocks are candidates: the text
// '缓存' appears in the stale block and in the unidentifiable one, so both
// enter the candidate pool and both are then filtered. (An earlier version of
// this test queried '空间', which never matched the unidentifiable block, so
// skipped was 1 — the assertion was wrong, not the count.)
// ★ The expected skip count is COMPUTED from the fixture, never written as a
// literal — a hardcoded 2 is the same class of unguarded number this project
// removed from the README: it silently stops describing the fixture the moment
// a block is added. `blockStates` is the independent source of truth for how
// many blocks are unusable in this space.
const expectedSkipped = counts.unknownSpace + counts.spaceMismatch
assert.ok(expectedSkipped > 0, 'the fixture must actually contain excluded blocks')
const hits = recallBlocks(store, '缓存', { topK: 10, fingerprint: 'fp-cur' })
assert.ok(hits.skipped > 0, `skipped must be non-zero when blocks are filtered; got ${hits.skipped}`)
assert.equal(
hits.skipped,
expectedSkipped,
`skipped must equal the number of unusable blocks in scope (${expectedSkipped})`,
)
assert.equal(
hits.length,
0,
'neither excluded block returns; the active and vectorless blocks do not match this query',
)
// And a query that matches a kept block still returns it, so `skipped === 2`
// above is not the result of everything being excluded.
const kept = recallBlocks(store, '当前空间', { topK: 10, fingerprint: 'fp-cur' })
assert.equal(kept.length, 1)
assert.equal(kept.skipped, 0, 'no skip when every candidate is in the active space')
} finally {
store.close()
}
})
test('F4: dim mismatch and fingerprint mismatch are counted separately', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedFourStates(store)
// A block whose vector is the wrong width for the active space. The remedies
// differ from a fingerprint mismatch (provider config vs re-embed), so F4
// requires them apart.
store.putBlock({ text: 'E 维度不符', vector: [1, 2], fingerprint: 'fp-cur' })
// ★ The active DIMENSION must be passed for the dim comparison to run — a
// count silently stuck at 0 because the caller omitted an argument is its own
// version of the fake-green this group is about.
const counts = store.blockStates('fp-cur', 3)
assert.equal(counts.dimensionMismatch, 1, 'the 2-dim row is a dimension mismatch')
assert.equal(counts.spaceMismatch, 1, 'and the stale-space row is still a space mismatch')
// Prove they are SEPARATE buckets rather than one merged number: a merged
// counter would report 2 for whichever field it used. Both are 1 here, so
// compare against the specific fields instead of against each other.
assert.equal(
counts.dimensionMismatch + counts.spaceMismatch,
2,
'two distinct blocks are in two distinct states',
)
// And they must not double-count the same row: the 2-dim row is fp-cur, so it
// is a dimension mismatch only; the stale row is 3-dim, so it is a space
// mismatch only. Neither contributes to the other's bucket.
assert.equal(counts.active, 1, 'only the correctly sized active block counts as active')
} finally {
store.close()
}
})
test('F3: counts are derived from the DB, not accumulated in memory', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedFourStates(store)
const before = store.blockStates('fp-cur', 3)
assert.equal(before.spaceMismatch, 1)
// Mutate the DB directly, behind the plugin's back. An in-memory accumulator
// would not notice; a derived count must.
//
// The row is moved from a stale space INTO the active one, so spaceMismatch
// drops — an UPDATE that merely renames one stale space to another would leave
// the count unchanged and the assertion would pass while proving nothing.
// (That was the first version of this test: fp-old -> fp-other is still a
// mismatch, so `1 !== 2` failed for the right reason but tested the wrong
// thing.)
const updated = store.raw()
.prepare("UPDATE memory_blocks SET fingerprint = 'fp-cur' WHERE text_content = 'B 旧空间 缓存'")
.run()
assert.equal(updated.changes, 1, 'the update really hit one row')
const after = store.blockStates('fp-cur', 3)
assert.equal(after.spaceMismatch, 0, 'the count must follow the database')
assert.equal(after.active, 2, 'and the row now counts as active')
store.raw().prepare('DELETE FROM memory_blocks WHERE text_content = ?').run('B 旧空间 缓存')
const afterDelete = store.blockStates('fp-cur', 3)
assert.equal(afterDelete.active, 1, 'and follow a delete too')
assert.equal(afterDelete.total, before.total - 1, 'total tracks the row count')
} finally {
store.close()
}
})
test('F3: skip counting covers the SYMBOL path (fusion.go:495 shape)', () => {
// If only the vector path counted, a stale block excluded by the symbol path
// would leave `skipped` at zero while the result silently lost a hit. That is
// the same half-coverage HA had.
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedFourStates(store)
// Symbol-only retrieval: no embedder involved at all.
const hits = recallBlocks(store, '旧空间', { topK: 10, fingerprint: 'fp-cur' })
assert.equal(hits.length, 0, 'the stale block is the only match and it is excluded')
assert.equal(hits.skipped, 1, 'and the symbol path counted the skip')
} finally {
store.close()
}
})
// ── F3/F4 mutations ───────────────────────────────────────────────────────
//
// Three independent mutations, because the accounting can fail in three ways:
// 1. counting accumulated in memory instead of derived from rows (D4)
// 2. counting wired to the vector path only, leaving the symbol path silent
// 3. dim mismatch collapsed into space mismatch (F4)
test('F3†: an in-memory accumulator does not follow the database', async () => {
// Faithful implementation of the FORBIDDEN design: count on write, answer from
// the counter, never re-read the rows. The mutation has to actually work, or the
// test proves nothing (a mutation that fails for the wrong reason is not
// evidence — same rule as everywhere else in this suite).
const ACC = `const __acc = { total: 0, noVector: 0, unknownSpace: 0, spaceMismatch: 0 }
`
await withMutatedCopy(
'store.js',
[
[`import { createHash } from 'node:crypto'`, `import { createHash } from 'node:crypto'\n${ACC}`],
[
' blockStates(activeFingerprint, activeDimension) {',
` blockStates(activeFingerprint, activeDimension) {
// MUTATED: answer from an accumulator instead of the rows (D4 violation)
return { ...__acc, active: 0, dimensionMismatch: 0 }`,
],
],
async (mod) => {
// Drive the accumulator through the module-level object the mutation added.
const store = new mod.BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'A', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'B', vector: [0, 1, 0], fingerprint: 'fp-old' })
const before = store.blockStates('fp-cur', 3)
// Change the database behind the plugin's back.
store.raw().prepare("UPDATE memory_blocks SET fingerprint = 'fp-cur' WHERE text_content = 'B'").run()
const after = store.blockStates('fp-cur', 3)
assert.deepEqual(
after,
before,
'the accumulator ignores the DB change',
)
// And it is not merely returning zeros: that would be a different bug.
assert.equal(before.total, 0, 'the accumulator was never fed, so it reports zeros')
// Now emulate F3's own assertion against these values: it demands
// spaceMismatch to change when the DB changes, which cannot happen here.
assert.throws(
() => assert.notEqual(after.spaceMismatch, before.spaceMismatch, 'must follow the DB'),
(err) => err.code === 'ERR_ASSERTION',
"F3's 'follow the database' assertion fails on the mutated module",
)
} finally {
store.close()
}
},
)
})
test('F3†symbol: counting only the vector path leaves the symbol path silent', async () => {
// The half-coverage failure. Mutate recallBlocks so `skipped` is only incremented
// in the embedding branch — emulated by zeroing it on the symbol path.
const { BlockStore: RealStore } = await import('../lib/store.js')
await withMutatedCopy(
'recall.js',
[[
` pool.delete(id)
skipped += 1`,
` pool.delete(id)
// MUTATED: a counter that only the (absent) vector path would bump
if (false) skipped += 1`,
]],
async (mod) => {
const store = new RealStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'B 旧空间 缓存', vector: [0, 1, 0], fingerprint: 'fp-old' })
// Symbol-only call: no signal passed, so the mutated counter stays 0.
const hits = mod.recallBlocks(store, '缓存', { topK: 10, fingerprint: 'fp-cur' })
assert.equal(hits.length, 0, 'the stale block is still excluded')
assert.equal(
hits.skipped,
0,
'under the mutation the skip goes uncounted even though a block was dropped',
)
// F3's symbol-path criterion asserts skipped === 1 here; on the mutated
// module it is 0, so that assertion would fail.
assert.throws(
() => assert.equal(hits.skipped, 1, 'symbol path must count the skip'),
(err) => err.code === 'ERR_ASSERTION',
)
} finally {
store.close()
}
},
)
})
test('F4†: merging dim mismatch into space mismatch breaks the split', async () => {
await withMutatedCopy(
'store.js',
[[
` counts.dimensionMismatch += 1
continue`,
` // MUTATED: collapse the dimension bucket into the space bucket
counts.spaceMismatch += 1
continue`,
]],
async (mod) => {
const store = new mod.BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'A', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'E 维度不符', vector: [1, 2], fingerprint: 'fp-cur' })
const counts = store.blockStates('fp-cur', 3)
assert.equal(counts.dimensionMismatch, 0, 'the bucket is empty under the mutation')
assert.equal(
counts.spaceMismatch,
1,
'and the row lands in the space bucket instead — which is exactly the merge F4 forbids',
)
// F4 asserts dimensionMismatch === 1; on the mutated module it is 0.
assert.throws(
() => assert.equal(counts.dimensionMismatch, 1, 'must be its own bucket'),
(err) => err.code === 'ERR_ASSERTION',
)
} finally {
store.close()
}
},
)
})
// ── D3/D5: the accounting reaches the tool surface and the prompt text ─────
//
// Two consumers, ONE source. `renderIndex()` and `stablemem_stats` both call
// `blockStates()`; neither keeps its own tally. If they did, the model would be
// told a number that contradicts the operator's report.
test('D3: stablemem_stats reports derived counts, and they are non-zero', async () => {
const { ctx, mem, done } = await makeMem({})
try {
await mem.commit({ text: 'A 当前空间', vector: [1, 0, 0], fingerprint: 'fp-cur' })
await mem.commit({ text: 'B 旧空间', vector: [0, 1, 0], fingerprint: 'fp-old' })
await mem.commit({ text: 'C 无向量' })
const result = await ctx.tools.execute({
callId: 's1',
name: 'stablemem_stats',
arguments: {},
signal: new AbortController().signal,
})
assert.equal(result.isError, false, `stats tool failed: ${JSON.stringify(result.value)}`)
const states = result.value.states
assert.equal(states.total, 3)
assert.equal(states.noVector, 1, 'the vectorless block is counted')
// ★ With NO active space declared there is nothing to be mismatched against:
// `spaceMismatch` requires a reference, so it is correctly 0 here and the
// labelled blocks classify as active. Asserting 2 here was the test being
// wrong, not the count — only a declared space can produce a mismatch.
assert.equal(states.spaceMismatch, 0, 'no active space ⇒ nothing can mismatch it')
assert.equal(states.active, 2, 'the two labelled blocks are usable as-is')
assert.equal(
result.value.space,
null,
'and the report says no space is configured, rather than implying one exists',
)
} finally {
await done()
}
})
test('D3: with a declared space, mismatch and unknownSpace are both non-zero', async () => {
// Complement to the test above: a mismatch can only be counted against a
// reference space, so this case configures one. Without this, the suite would
// never exercise a non-zero `spaceMismatch` through the tool surface.
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { ctx, mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: 'http://127.0.0.1:1/embed', dimension: 3 }),
})
try {
// Explicit vectors, so no network call is made.
await mem.commit({ text: 'A 当前', vector: [1, 0, 0], fingerprint: mem.space().fingerprint })
await mem.commit({ text: 'B 旧空间', vector: [0, 1, 0], fingerprint: 'fp-retired' })
await mem.commit({ text: 'C 无向量' })
await mem.commit({ text: 'D 无名向量' })
mem.store.raw().prepare("UPDATE memory_blocks SET vector = ? WHERE text_content = 'D 无名向量'")
.run(JSON.stringify([0, 0, 1]))
const result = await ctx.tools.execute({
callId: 's4', name: 'stablemem_stats', arguments: {}, signal: new AbortController().signal,
})
const states = result.value.states
assert.equal(states.spaceMismatch, 1, 'the retired-space block is mismatched')
assert.equal(states.unknownSpace, 1, 'the unlabelled block has its own state')
assert.equal(states.noVector, 1)
assert.equal(states.active, 1)
assert.equal(states.total, 4)
assert.equal(result.value.space.dimension, 3)
} finally {
await done()
}
})
test('D3: mutating the DB changes what the tool reports (derived, not cached)', async () => {
const { ctx, mem, done } = await makeMem({})
try {
await mem.commit({ text: 'A 无向量' })
const first = await ctx.tools.execute({
callId: 's2', name: 'stablemem_stats', arguments: {}, signal: new AbortController().signal,
})
assert.equal(first.value.states.noVector, 1)
// Write a block straight into the store, outside the tool path.
mem.store.putBlock({ text: 'B 无向量' })
const second = await ctx.tools.execute({
callId: 's3', name: 'stablemem_stats', arguments: {}, signal: new AbortController().signal,
})
assert.equal(second.value.states.noVector, 2, 'the tool must follow the database')
} finally {
await done()
}
})
test('D5: the prompt index states how many blocks cannot be recalled semantically', async () => {
// ★ Needs a CONFIGURED space: with no embedder the advisory is deliberately
// silent (D5d / W11e), so asserting it there would contradict that ruling.
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: 'http://127.0.0.1:1/embed', dimension: 3 }),
})
try {
// One embedded block (so the space exists) and one vectorless block.
await mem.commit({ text: 'embedded', vector: [1, 0, 0], fingerprint: mem.space().fingerprint })
await mem.commit({ text: 'not embedded' })
const text = mem.renderIndex()
assert.ok(text.includes('2 durable note'), text)
assert.ok(
/Unavailable for semantic recall/.test(text),
`the index must say unavailable blocks exist, not just count notes; got: ${text}`,
)
assert.ok(/1 not yet embedded/.test(text), `and say which state and how many; got: ${text}`)
} finally {
await done()
}
})
test('D5†: dropping the unavailable sentence makes D5 red', async () => {
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: 'http://127.0.0.1:1/embed', dimension: 3 }),
})
try {
await mem.commit({ text: 'embedded', vector: [1, 0, 0], fingerprint: mem.space().fingerprint })
await mem.commit({ text: 'not embedded' })
const oldStyle = '[Memory · dsh-memory] 2 durable note(s) in block memory.'
assert.ok(!/Unavailable for semantic recall/.test(oldStyle), 'the old wording carries no warning')
assert.ok(
/Unavailable for semantic recall/.test(mem.renderIndex()),
'the current wording does — so D5 fails on the old shape',
)
} finally {
await done()
}
})
// ── F8: a mis-sized vector is a HARD error when the width is declared ──────
//
// ★ The positive predicate below is shared by the unmutated case and by every
// F8 mutant, so a mutant can only pass by satisfying the SAME requirement the
// real code satisfies. Re-writing the requirement inside the mutant's body
// ("under the mutation the bad vector lands") is a self-test: it asserts the
// mutated behaviour and therefore stays green exactly when the criterion should
// fail.
function assertExplicitWidthRefused(store) {
assert.throws(
() => store.putBlock({ text: 'x', vector: [1, 2], fingerprint: 'fp-a', expectedDimension: 3 }),
(err) => /dimension 2 does not match the active space's 3/.test(err.message),
'a 2-wide vector in a 3-wide space must be refused, not stored and counted later',
)
assert.equal(store.countBlocks(), 0, 'nothing was written')
}
//
// criteria:165 says "provider returns a dimension ≠ the configured one ⇒ hard
// error, never truncate or pad". Until now only the adapter layer enforced it
// (H3b); the write path silently degraded, so a bad vector could land and be
// counted later — accounting, not validation.
//
// `expectedDimension` is opt-in, so the embed-less and migration paths are not
// forced to invent a width.
test('F8: putBlock refuses a vector whose width differs from the declared space', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
assertExplicitWidthRefused(store)
// The matching width is accepted.
assert.equal(
store.putBlock({ text: 'ok', vector: [1, 2, 3], fingerprint: 'fp-a', expectedDimension: 3 }).created,
true,
)
// Without a declared width the check is inert (migration / fixture path).
assert.equal(
store.putBlock({ text: 'no-dim', vector: [1, 2], fingerprint: 'fp-a' }).created,
true,
'omitting expectedDimension must not invent a width',
)
} finally {
store.close()
}
})
test('F8: commit refuses an explicit vector of the wrong width', async () => {
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: 'http://127.0.0.1:1/embed', dimension: 3 }),
})
try {
await assert.rejects(
() => mem.commit({ text: 'x', vector: [1, 2], fingerprint: mem.space().fingerprint }),
/does not match the active space/,
'an explicit mis-sized vector is a caller bug and must surface here',
)
assert.equal(mem.store.countBlocks(), 0)
} finally {
await done()
}
})
test('F8: a provider returning the wrong width still degrades (not a caller bug)', async () => {
// The distinction that keeps F8 from breaking W4: `commit` without an explicit
// vector is EMBEDDING, and a provider misbehaving is not the caller's fault. It
// degrades to "stored without a vector" (W4) rather than throwing. F8 is about a
// caller handing over a vector it claims is good.
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { mem, done } = await makeMem({
embedder: {
async embedText() { return [1, 2] }, // provider lies about the width
info: () => ({ fingerprint: 'fp-x', dimension: 3 }),
},
})
try {
const r = await mem.commit({ text: 'x' })
assert.equal(r.embedded, false, 'a misbehaving provider degrades, it does not throw')
// ★ Counting rows, not picking one: `allBlocks(1)[0]` is satisfied by any
// single row, so a regression that stopped writing on the degraded path
// entirely would still be green here. F8/W4 require the block to LAND.
assert.equal(mem.store.countBlocks(), 1, 'the block is still written (degrade ≠ drop)')
assert.equal(mem.store.allBlocks(1)[0].vector, '', 'and no bad vector is stored')
} finally {
await done()
}
})
test('F8†: softening the write-side width check bites the positive criterion red', async (t) => {
// ★ The anchor must appear EXACTLY ONCE in lib/store.js. `withMutatedCopy`
// already fails loudly when it is missing; the count check makes "appears
// twice, first match mutated, second silently left intact" impossible too.
const { readFileSync } = await import('node:fs')
const source = readFileSync(new URL('../lib/store.js', import.meta.url), 'utf8')
assert.equal(
source.split(' if (actual !== expectedDimension) {').length - 1,
1,
'the F8† anchor must occur exactly once in lib/store.js',
)
await withMutatedCopy(
'store.js',
[[
` if (actual !== expectedDimension) {`,
` if (false) { // MUTATED: no width enforcement`,
]],
async (mod) => {
const store = new mod.BlockStore({ path: ':memory:' }).open()
try {
// ★ The SAME positive criterion, run on the mutant. It must throw: on the
// mutant the write is admitted, so `assert.throws` sees no error and
// fails with ERR_ASSERTION.
assert.throws(() => assertExplicitWidthRefused(store), (err) => {
assert.equal(err.code, 'ERR_ASSERTION', 'the criterion must fail as an assertion')
t.diagnostic(`F8† RAW RED >>> ${String(err.message).split('\n')[0]}`)
return true
}, 'F8† must break the unchanged F8 criterion')
// Corroboration: the mutation really does admit the bad vector. This is
// reported as a diagnostic, never as the thing that "bites" — asserting
// it would be the self-test that made the previous version useless.
const probe = store.putBlock({ text: 'probe', vector: [1, 2], fingerprint: 'fp-a', expectedDimension: 3 })
t.diagnostic(`F8† mutant admitted the bad vector: created=${probe.created}, blocks=${store.countBlocks()}`)
} finally {
store.close()
}
},
)
})
test('F8†b: a degraded provider write that drops the block bites countBlocks red', async (t) => {
// ★ Mutation on lib/index.js: the degraded branch (provider returned a wrong
// width) stops persisting. `embedded === false` and `vector === ''` both stay
// green under this mutation — only the row count catches it, which is exactly
// what the `allBlocks(1)[0]` shape could not do.
await withMutatedCopy(
'index.js',
[[
` if (!Array.isArray(vector) || vector.length !== space.dimension) {
return { ...this.store.putBlock(input), embedded: false }
}`,
` if (!Array.isArray(vector) || vector.length !== space.dimension) {
return { id: '', created: false, embedded: false } // MUTATED: degrade by dropping
}`,
]],
async (mod) => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'f8b-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = mod.apply(ctx, {
path: join(dir, 'm.db'),
embedder: { async embedText() { return [1, 2] }, info: () => ({ fingerprint: 'fp-x', dimension: 3 }) },
})
const r = await mem.commit({ text: 'x' })
// The two assertions that survive the mutation — reported, not relied on.
t.diagnostic(`F8†b still-green under mutation: embedded=${r.embedded}, blocks=${mem.store.countBlocks()}`)
assert.throws(() => {
assert.equal(
mem.store.countBlocks(),
1,
'the block must still be written on a degraded provider write',
)
assert.equal(mem.store.allBlocks(1)[0]?.vector, '', 'and carry no bad vector')
}, (err) => {
assert.equal(err.code, 'ERR_ASSERTION', 'only the row-count criterion may bite')
t.diagnostic(`F8†b RAW RED >>> ${String(err.message).split('\n')[0]}`)
return true
}, 'F8†b must break the unchanged provider-degradation criterion')
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
test('F3 performance guard: blockStates source does not parse vector payloads on reads', async () => {
// The old implementation did `JSON.parse(row.vector).length` for every row;
// at 2000 blocks x 2048 dims that measured ~354ms per renderIndex call, ~255ms
// of it pure parsing. `vector_dim` is now the indexed row fact. This static
// guard prevents a later "simplification" from putting JSON.parse back into
// the hot path; migration is allowed to parse once, blockStates is not.
const { readFileSync } = await import('node:fs')
const source = readFileSync(new URL('../lib/store.js', import.meta.url), 'utf8')
const start = source.indexOf(' blockStates(activeFingerprint, activeDimension) {')
assert.notEqual(start, -1)
const end = source.indexOf('\n }\n\n /**', start)
assert.notEqual(end, -1)
const body = source.slice(start, end)
assert.equal(body.includes('JSON.parse'), false, 'blockStates must use vector_dim, not parse vector JSON')
assert.match(body, /vector_dim/, 'blockStates must read the stored dimension')
})
// ══════════════════════════════════════════════════════════════════════════
// 返工 ①: the per-call failure must be QUERYABLE, not merely returned
// ══════════════════════════════════════════════════════════════════════════
//
// `degraded` on the result array is a RETURN VALUE: the caller (the model) sees
// it once and it is gone. criteria:282 requires `last_recall` on the stats
// surface — an OPERATOR must be able to ask "did the last recall fail, and why".
// These are different questions and neither substitutes for the other, exactly as
// `states` could not substitute for `skipped`.
//
// It deliberately does NOT go into the four states: those are library state
// derived from rows, and a per-call event would make two reads at the same
// instant disagree — the failure mode D4 exists to prevent.
test('D3b: stats().last_recall exposes the most recent recall degradation', async () => {
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { ctx, mem, done } = await makeMem({
embedder: {
async embedText() { throw new Error('provider exploded') },
info: () => ({ fingerprint: 'fp-x', dimension: 3 }),
},
})
try {
await mem.commit({ text: '值班室分机号 4324' })
// Cause one failing recall through the production entry point.
const hits = await mem.recall('4324')
assert.equal(hits.degraded, 'embed_error', 'the call itself degrades')
const result = await ctx.tools.execute({
callId: 'lr', name: 'stablemem_stats', arguments: {}, signal: new AbortController().signal,
})
assert.equal(result.isError, false)
const last = result.value.last_recall
assert.ok(last, 'stats() must carry a last_recall section')
assert.equal(last.mode, 'embed_error', 'and report the degradation that actually happened')
assert.match(last.embed_error, /provider exploded/, 'with the underlying reason')
assert.equal(typeof last.skipped_fp, 'number')
assert.equal(typeof last.skipped_dim, 'number')
} finally {
await done()
}
})
test('D3b: a clean recall overwrites last_recall with its own outcome', async () => {
const { createHttpEmbedder } = await import('../lib/embed/http.js')
// A healthy embedder, and blocks written with explicit vectors so no network
// is needed for the commit path.
const { ctx, mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: 'http://127.0.0.1:1/embed', dimension: 3 }),
})
try {
const fp = mem.space().fingerprint
await mem.commit({ text: 'A', vector: [1, 0, 0], fingerprint: fp })
// ★ Capture `fp` BEFORE swapping the embedder. Writing
// `info: () => mem.space()...` makes info() call space() which calls info():
// infinite recursion. (Cost me a stack overflow to find.)
mem.embedder = {
async embedText() { return [1, 0, 0] },
info: () => ({ fingerprint: fp, dimension: 3 }),
}
await mem.recall('A')
const result = await ctx.tools.execute({
callId: 'lr2', name: 'stablemem_stats', arguments: {}, signal: new AbortController().signal,
})
// ★ A healthy recall does NOT report 'ok': group A deliberately labels the
// success path `vector_ready_unused` because the vector is computed but not
// yet scored. Asserting 'ok' here was my mistake — the label that matters is
// "no failure", and `embed_error === null` is the honest way to say it.
assert.notEqual(result.value.last_recall.mode, 'embed_error', 'no failure is recorded')
assert.equal(result.value.last_recall.embed_error, null, 'and no error reason either')
assert.equal(result.value.last_recall.mode, 'vector_ready_unused', 'the group-A success label')
} finally {
await done()
}
})
test('D3b†: not recording last_recall leaves the operator blind', async () => {
await withMutatedCopy(
'index.js',
[[
' this.lastRecall = {',
' // MUTATED: computed but never stored\n const __unused = {',
]],
async (mod) => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'lr-'))
const ctx = new Context(); new SystemPrompt(ctx, {}); new ToolRuntime(ctx, {})
try {
const mem = mod.apply(ctx, { path: join(dir, 'm.db') })
await mem.commit({ text: 'x' })
await mem.recall('x')
// With the assignment removed, `lastRecall` stays at its initial null.
assert.equal(
mem.stats().last_recall,
null,
'the mutation leaves no record — so D3b (which requires a populated section) fails',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
// ══════════════════════════════════════════════════════════════════════════
// 返工 ②: the index must describe each unavailable state in its own terms
// ══════════════════════════════════════════════════════════════════════════
//
// The old tail collapsed all four states into one sentence ending "they are still
// searchable by exact text". That is true for `noVector` — the block has no
// vector, but text retrieval still reaches it — and FALSE for `spaceMismatch` on
// the production recall path, which F2 excludes:
//
// production recall (with fp): hits=0, skipped=1
// store-level browse (no fp): hits=1
//
// Same distinction F6 needed three paragraphs to settle; the index was asserting
// the wrong half of it.
/** Render the index with a controlled space/config, via a real StableMem. */
async function renderIndexFor({ space, store, count, forceReport = false }) {
const { StableMem } = await import('../lib/index.js')
const { Context } = await import('@deepseek-ai/cordis')
const ctx = new Context()
const mem = new StableMem(ctx, {
store,
embedder: space === null ? null : { embedText: async () => [1], info: () => space },
// `forceReport` lets a D5 test assert the wording for a state that would
// otherwise be silent (no embedder is a standing fact, see D5d).
reportNoVectorAlways: forceReport,
})
void count
const text = mem.renderIndex()
await ctx.fiber.dispose()
return text
}
test('D5b: spaceMismatch is described as browse-only, never as "still searchable"', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
const store = new RealStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'A 当前', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'B 旧空间', vector: [0, 1, 0], fingerprint: 'fp-old' })
assert.equal(store.blockStates('fp-cur', 3).spaceMismatch, 1)
const text = await renderIndexFor({
space: { fingerprint: 'fp-cur', dimension: 3 }, store,
})
assert.ok(/browse/i.test(text), `a mismatched block is reachable only by browse; got: ${text}`)
assert.ok(
!/still searchable by exact text/i.test(text),
`the unqualified "still searchable" claim must be gone; got: ${text}`,
)
} finally {
store.close()
}
})
test('D5c: noVector is described as text-recallable, which IS true', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
const { recallBlocks } = await import('../lib/recall.js')
const store = new RealStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324' })
// Establish the fact the wording rests on, rather than assuming it.
assert.ok(recallBlocks(store, '4324', { topK: 3 }).length > 0, 'text recall really does reach it')
const text = await renderIndexFor({ space: null, store, forceReport: true })
assert.ok(/text/i.test(text), `the not-yet-embedded state must mention text recall; got: ${text}`)
} finally {
store.close()
}
})
test('D5d: with no embedder configured, the index stays silent every turn (W11e shape)', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
const store = new RealStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'a' })
store.putBlock({ text: 'b' })
// No embedder is the SHIPPING DEFAULT, not an incident: repeating a standing
// fact every turn is exactly what W11e ruled out for degradation notices.
const text = await renderIndexFor({ space: null, store })
assert.ok(/2 durable note/.test(text), `the count line stays: ${text}`)
assert.ok(
!/cannot currently be recalled/i.test(text),
`a standing configuration fact must not be repeated each turn; got: ${text}`,
)
} finally {
store.close()
}
})
test('D5b†: restoring the single-sentence tail trips D5b', () => {
const oldTail = '2 of them cannot currently be recalled semantically; they are still searchable by exact text.'
assert.match(oldTail, /still searchable by exact text/i, 'the old sentence makes the claim')
assert.ok(!/browse/i.test(oldTail), 'and never mentions browse — so D5b fails on it')
})
// ══════════════════════════════════════════════════════════════════════════
// 返工 ③: the trigger that keeps vector_dim honest needs a DIRECT guard
// ══════════════════════════════════════════════════════════════════════════
//
// The triggers are indirectly covered (stripping their WHEN makes F3/D3 red),
// but nothing exercises a RAW writer — a repair script, a migration, an operator
// with sqlite3 — which is the case the trigger exists for. `putBlock` keeps the
// column right by itself, so every existing test would pass with no triggers at
// all were it not for the two that happen to read the column afterwards.
test('MIG5: a raw UPDATE of vector has vector_dim corrected by the trigger', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: 'x', vector: [1, 2, 3], fingerprint: 'fp-a', expectedDimension: 3 })
assert.equal(store.raw().prepare('SELECT vector_dim FROM memory_blocks').get().vector_dim, 3)
// A raw writer sets a 5-wide vector but leaves vector_dim at 3 — the state the
// column must be incapable of representing.
store.raw()
.prepare('UPDATE memory_blocks SET vector = ?')
.run(JSON.stringify([1, 2, 3, 4, 5]))
const dim = store.raw().prepare('SELECT vector_dim FROM memory_blocks').get().vector_dim
assert.equal(dim, 5, 'the trigger must pull vector_dim back to the real length')
// And clearing the vector resets it to the "no vector" sentinel.
store.raw().prepare("UPDATE memory_blocks SET vector = ''").run()
assert.equal(
store.raw().prepare('SELECT vector_dim FROM memory_blocks').get().vector_dim,
0,
'an emptied vector must read as 0, not as its stale width',
)
} finally {
store.close()
}
})
test('MIG5: a raw INSERT with a mismatched vector_dim is corrected on the way in', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
// Insert directly with a WRONG vector_dim, the way a hand-written repair would.
store.raw()
.prepare(`INSERT INTO memory_blocks (id, modality, text_content, vector, vector_dim, fingerprint, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
.run('raw-1', 'text', 'raw inserted', JSON.stringify([1, 2]), 99, 'fp-a', Date.now(), Date.now())
assert.equal(
store.raw().prepare("SELECT vector_dim FROM memory_blocks WHERE id = 'raw-1'").get().vector_dim,
2,
'the insert trigger must overwrite a lying vector_dim',
)
} finally {
store.close()
}
})
test('MIG5†: dropping either trigger lets a lying vector_dim survive', async () => {
// Two triggers, two independent guards: removing only the INSERT one must not
// hide behind the UPDATE one, and vice versa.
const { BlockStore: RealStore } = await import('../lib/store.js')
// (a) drop the UPDATE trigger
const store = new RealStore({ path: ':memory:' }).open()
try {
store.raw().exec('DROP TRIGGER IF EXISTS memory_blocks_vector_dim_update')
store.putBlock({ text: 'y', vector: [1, 2, 3], fingerprint: 'fp-a', expectedDimension: 3 })
store.raw().prepare('UPDATE memory_blocks SET vector = ?').run(JSON.stringify([1, 2, 3, 4, 5]))
assert.equal(
store.raw().prepare('SELECT vector_dim FROM memory_blocks').get().vector_dim,
3,
'without the UPDATE trigger the column lies — so MIG5 fails on it',
)
} finally {
store.close()
}
const store2 = new RealStore({ path: ':memory:' }).open()
try {
store2.raw().exec('DROP TRIGGER IF EXISTS memory_blocks_vector_dim_insert')
store2.raw()
.prepare(`INSERT INTO memory_blocks (id, modality, text_content, vector, vector_dim, fingerprint, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
.run('raw-2', 'text', 'r', JSON.stringify([1, 2]), 99, 'fp-a', Date.now(), Date.now())
assert.equal(
store2.raw().prepare("SELECT vector_dim FROM memory_blocks WHERE id = 'raw-2'").get().vector_dim,
99,
'without the INSERT trigger a hand-written width survives — so MIG5 fails on it',
)
} finally {
store2.close()
}
})
// ══════════════════════════════════════════════════════════════════════════
// 返工 ④: skipped_dim must be able to be non-zero, or it must not exist
// ══════════════════════════════════════════════════════════════════════════
//
// `last_recall.skipped_dim` was hardcoded to 0 with the comment "reported as 0
// rather than guessed", and the only assertion on it was `typeof === 'number'` —
// which a constant satisfies forever. That is the same defect as the hardcoded
// `skipped === 2` removed earlier: a field that can never be non-zero turns F4's
// "count dimension mismatches separately" into decoration.
//
// Ruling taken: option (a) — the recall path gains a dimension filter, so the
// field can report something real. The alternative (drop the field) would leave
// criteria:161 describing a separation that only exists in blockStates.
test('F4b: a same-space block of the wrong width is skipped as a DIMENSION skip', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
// Same fingerprint (so it is NOT a space mismatch), but 2-wide in a 3-wide
// space. Only a dimension check can exclude this one.
store.putBlock({ text: '值班室分机号 4324 窄向量', vector: [1, 2], fingerprint: 'fp-cur' })
store.putBlock({ text: '值班室分机号 4324 正常', vector: [1, 0, 0], fingerprint: 'fp-cur' })
const hits = recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-cur', dimension: 3 })
assert.equal(hits.length, 1, 'only the correctly sized block is recalled')
assert.ok(hits.some((h) => h.text.includes('正常')))
assert.equal(hits.skippedDim, 1, 'the narrow block is counted as a DIMENSION skip')
assert.equal(hits.skipped, 0, 'and NOT as a space skip — F4 keeps them apart')
} finally {
store.close()
}
})
test('F4b: both skip kinds are reported side by side without double counting', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '值班室分机号 4324 旧空间', vector: [0, 1, 0], fingerprint: 'fp-old' })
store.putBlock({ text: '值班室分机号 4324 窄向量', vector: [1, 2], fingerprint: 'fp-cur' })
store.putBlock({ text: '值班室分机号 4324 正常', vector: [1, 0, 0], fingerprint: 'fp-cur' })
const hits = recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-cur', dimension: 3 })
assert.equal(hits.length, 1)
assert.equal(hits.skipped, 1, 'one space skip')
assert.equal(hits.skippedDim, 1, 'one dimension skip')
} finally {
store.close()
}
})
test('F4b: last_recall.skipped_dim reflects a real dimension skip', async () => {
const { createHttpEmbedder } = await import('../lib/embed/http.js')
const { mem, done } = await makeMem({
embedder: createHttpEmbedder({ endpoint: 'http://127.0.0.1:1/embed', dimension: 3 }),
})
try {
const fp = mem.space().fingerprint
await mem.commit({ text: '正常 4324', vector: [1, 0, 0], fingerprint: fp })
// ★ The narrow row is written RAW, not through commit: F8 (correctly) refuses a
// mis-sized explicit vector, so a legacy/repair-written row is the realistic way
// this state exists in a live database.
mem.store.putBlock({ text: '窄 4324', vector: [1, 2], fingerprint: fp })
await mem.recall('4324')
assert.equal(mem.lastRecall.skipped_dim, 1, 'the statistic must be able to be non-zero')
assert.equal(mem.lastRecall.skipped_fp, 0)
} finally {
await done()
}
})
test('F4b†: dropping the dimension filter makes skipped_dim permanently zero again', async () => {
await withMutatedCopy(
'recall.js',
[[
' if (Number.isInteger(dimension) && dimension > 0) {',
' if (false) { // MUTATED: no dimension filter',
]],
async (mod) => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
store.putBlock({ text: '窄 4324', vector: [1, 2], fingerprint: 'fp-cur' })
const hits = mod.recallBlocks(store, '4324', { topK: 10, fingerprint: 'fp-cur', dimension: 3 })
assert.equal(hits.skippedDim, 0, 'without the filter the narrow block is not counted')
assert.equal(
hits.length,
1,
'and it is returned — so F4b (which excludes it) fails on the mutated module',
)
} finally {
store.close()
}
},
)
})
// ── D5e / D5f: the two states that had no wording criterion ───────────────
//
// `renderUnavailableTail` emits a clause per state, but only `spaceMismatch`
// (D5b) and `noVector` (D5c) had one. Deleting the `unknownSpace` or
// `dimensionMismatch` clause left the whole suite green — those two were
// decoration in the renderer, exactly like the constant `skipped_dim` was.
//
// Each state gets its own clause assertion plus its own suppression mutation,
// because a shared "the tail mentions something" test would keep passing while
// either clause silently disappeared.
test('D5e: unknownSpace is named as a labelling problem, not a reachability one', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
const store = new RealStore({ path: ':memory:' }).open()
try {
// A block with a vector but no fingerprint: reachable by no semantic path,
// and its remedy is re-embedding, not browsing.
store.putBlock({ text: 'A 当前', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'D 无名向量' })
store.raw().prepare("UPDATE memory_blocks SET vector = ? WHERE text_content = 'D 无名向量'")
.run(JSON.stringify([0, 0, 1]))
assert.equal(store.blockStates('fp-cur', 3).unknownSpace, 1, 'the fixture is in the right state')
const text = await renderIndexFor({ space: { fingerprint: 'fp-cur', dimension: 3 }, store })
assert.ok(
/1 without a space label/.test(text),
`the unknownSpace clause must state the count and the reason; got: ${text}`,
)
// It must NOT promise browse or exact text: that claim belongs to `noVector`
// and is false here (no space label means no semantic participation at all).
assert.ok(
!/browse/i.test(text) && !/exact text/i.test(text),
`unknownSpace must not borrow noVector's reachability claim; got: ${text}`,
)
} finally {
store.close()
}
})
test('D5f: dimensionMismatch points at the configuration, not at browsing', async () => {
const { BlockStore: RealStore } = await import('../lib/store.js')
const store = new RealStore({ path: ':memory:' }).open()
try {
// Same fingerprint (so NOT a space mismatch), wrong width. Written raw because
// F8 rightly refuses a mis-sized explicit vector through putBlock/commit.
store.putBlock({ text: 'A 当前', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'E 宽度不符', vector: [1, 2], fingerprint: 'fp-cur' })
assert.equal(store.blockStates('fp-cur', 3).dimensionMismatch, 1, 'right state')
const text = await renderIndexFor({ space: { fingerprint: 'fp-cur', dimension: 3 }, store })
assert.ok(
/1 whose width does not match this provider/.test(text),
`the dimensionMismatch clause must state the count and the reason; got: ${text}`,
)
assert.ok(
/fix the config/i.test(text),
`and must point at the configuration remedy; got: ${text}`,
)
assert.ok(
!/browse/i.test(text),
`a width mismatch is not reachable by browsing either; got: ${text}`,
)
} finally {
store.close()
}
})
test('D5e†: deleting the unknownSpace clause must be caught', async () => {
await withMutatedCopy(
'index.js',
[[
` if (states.unknownSpace > 0) {
clauses.push(\`\${states.unknownSpace} without a space label (semantic recall unavailable)\`)
}`,
` // MUTATED: unknownSpace clause removed`,
]],
async (mod) => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'd5e-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = mod.apply(ctx, { path: join(dir, 'm.db') })
await mem.commit({ text: 'A', vector: [1, 0, 0], fingerprint: 'fp-cur' })
await mem.commit({ text: 'D 无名' })
mem.store.raw().prepare("UPDATE memory_blocks SET vector = ? WHERE text_content = 'D 无名'")
.run(JSON.stringify([0, 0, 1]))
const text = mem.renderIndex()
assert.equal(
/without a space label/.test(text),
false,
'the mutation really removes the clause — so D5e (which requires it) fails',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
test('D5f†: deleting the dimensionMismatch clause must be caught', async () => {
await withMutatedCopy(
'index.js',
[[
` if (states.dimensionMismatch > 0) {
clauses.push(
\`\${states.dimensionMismatch} whose width does not match this provider (fix the config, then re-embed)\`,
)
}`,
` // MUTATED: dimensionMismatch clause removed`,
]],
async (mod) => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'd5f-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = mod.apply(ctx, { path: join(dir, 'm.db') })
await mem.commit({ text: 'A', vector: [1, 0, 0], fingerprint: 'fp-cur' })
mem.store.putBlock({ text: 'E 窄', vector: [1, 2], fingerprint: 'fp-cur' })
const text = mem.renderIndex()
assert.equal(
/does not match this provider/.test(text),
false,
'the mutation really removes the clause — so D5f (which requires it) fails',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
},
)
})
// ── F7: store-level inventory grouped by fingerprint (D4 / D7) ─────────────
//
// One seed, one criterion, three mutants. The criterion below is the ORIGINAL
// positive assertion, reused verbatim against each mutated copy of lib/store.js:
// a mutant "bites" only when this criterion throws ERR_ASSERTION on it, which
// also satisfies the rework discipline ("the variant must change lib/, and the
// red must come from the real criterion").
function seedF7(store, withCentroids = true) {
store.putBlock({ text: 'F7 current', vector: [1, 0, 0], fingerprint: 'fp-cur' })
store.putBlock({ text: 'F7 retired one', vector: [1, 0], fingerprint: 'fp-old' })
store.putBlock({ text: 'F7 retired two', vector: [0, 1], fingerprint: 'fp-old' })
store.putBlock({ text: 'F7 plain' })
store.putBlock({ text: 'F7 legacy' })
// A pre-declaration row (unlabelled but vectored) must stay in its own bucket.
store.raw().prepare("UPDATE memory_blocks SET vector = '[0,0,1]' WHERE text_content = 'F7 legacy'").run()
if (withCentroids) {
for (const [fingerprint, dim] of [['fp-cur', 3], ['fp-old', 2]]) {
store.saveCentroid({ fingerprint, dim, vectorCount: 1, mean: Array(dim).fill(0), isAnomalous: false })
}
}
}
function assertF7(report) {
assert.deepEqual(report.blocks, { total: 5, with_vector: 4, no_vector: 1 })
assert.deepEqual(report.spaces, {
active: { fingerprint: 'fp-cur', dim: 3 },
by_fingerprint: [
{ fingerprint: 'fp-cur', dim: 3, blocks: 1, status: 'active' },
{ fingerprint: 'fp-old', dim: 2, blocks: 2, status: 'stale' },
],
no_fingerprint: 1,
}, 'F7: two spaces must coexist with distinct blocks, dims and statuses')
}
function assertF7NoCentroid(store) {
const report = store.vectorStats('fp-cur')
assert.equal(report.spaces.active.dim, null, 'D7-b: active dim without a centroid is null')
assert.deepEqual(
report.spaces.by_fingerprint.map((row) => row.dim), [null, null],
'D7-b: grouped dim without a centroid is null — blocks are never scanned for it',
)
}
function assertF7NoDecode(store) {
const parse = JSON.parse
let decodes = 0
JSON.parse = (...args) => { decodes += 1; return parse(...args) }
try {
store.vectorStats('fp-cur')
} finally {
JSON.parse = parse
}
assert.equal(decodes, 0, 'D7-a: the default inventory decodes zero vector payloads')
}
test('F7: the tool groups active and non-zero stale blocks with centroid dims', async () => {
const { ctx, mem, done } = await makeMem({
embedder: { info: () => ({ fingerprint: 'fp-cur', dimension: 3 }), embedText: async () => [1, 0, 0] },
})
try {
seedF7(mem.store)
const result = await ctx.tools.execute({
callId: 'f7', name: 'stablemem_stats', arguments: {}, signal: new AbortController().signal,
})
assert.equal(result.isError, false, `stats tool failed: ${JSON.stringify(result.value)}`)
assertF7(result.value)
assert.equal('byFingerprint' in result.value, false, 'no sibling alias beside the agreed shape')
assert.ok(result.value.states.total === 5 && typeof result.value.last_recall === 'object',
'the accepted D3/D6 sections are untouched by this slice')
} finally { await done() }
})
test('F7-D4: the inventory follows raw DB edits and survives a reopen', () => {
const dir = mkdtempSync(join(tmpdir(), 'f7-'))
const path = join(dir, 'm.db')
let store = new BlockStore({ path }).open()
try {
seedF7(store)
assertF7(store.vectorStats('fp-cur'))
store.raw().prepare("DELETE FROM memory_blocks WHERE fingerprint = 'fp-old'").run()
assert.deepEqual(store.vectorStats('fp-cur').spaces.by_fingerprint,
[{ fingerprint: 'fp-cur', dim: 3, blocks: 1, status: 'active' }],
'a raw DELETE is reflected on the next call — nothing was cached')
store.close()
store = new BlockStore({ path }).open()
assert.deepEqual(store.vectorStats('fp-cur').blocks, { total: 3, with_vector: 2, no_vector: 1 },
'a restart does not zero the inventory: it is derived, not accumulated')
} finally { store.close(); rmSync(dir, { recursive: true, force: true }) }
})
test('F7-D7: the default path decodes nothing and absent centroids report null', () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
seedF7(store, false)
assertF7NoDecode(store)
assertF7NoCentroid(store)
assert.equal(store.vectorStats('fp-cur').spaces.by_fingerprint.length, 2,
'and the groups still exist without a centroid — dim is the only thing unknown')
} finally { store.close() }
})
// The three required mutants. Each one changes lib/, then runs the SAME positive
// criterion; the criterion must throw ERR_ASSERTION.
for (const [name, replacements, withCentroids, criterion] of [
[
'M-F7',
[[
'by_fingerprint: groups.map((row) =>',
'by_fingerprint: groups.filter((row) => row.fingerprint === activeFingerprint).map((row) =>',
]],
true,
(store) => assertF7(store.vectorStats('fp-cur')),
],
[
'D7-a',
[[
' const groups = db.prepare(`SELECT b.fingerprint',
' const decodeVector = JSON.parse\n' +
' for (const row of db.prepare("SELECT vector FROM memory_blocks WHERE length(vector) > 0").all()) decodeVector(row.vector)\n' +
' const groups = db.prepare(`SELECT b.fingerprint',
]],
false,
assertF7NoDecode,
],
[
'D7-b',
[[
' dim: row.dim,',
' dim: JSON.parse(db.prepare("SELECT vector FROM memory_blocks WHERE fingerprint = ? LIMIT 1").get(row.fingerprint).vector).length,',
]],
false,
assertF7NoCentroid,
],
]) {
test(`F7† ${name}: the unchanged positive criterion bites red`, async (t) => {
await withMutatedCopy('store.js', replacements, async ({ BlockStore: MutantStore }) => {
const store = new MutantStore({ path: ':memory:' }).open()
try {
seedF7(store, withCentroids)
assert.throws(() => criterion(store), (err) => {
assert.equal(err.code, 'ERR_ASSERTION', 'only an assertion failure counts as a bite')
t.diagnostic(`${name} RAW RED >>> ${String(err.message).split('\n')[0]}`)
return true
}, `${name} must break the original positive criterion`)
} finally { store.close() }
})
})
}