Files
HologramAgent-DSH-Memory-Port/test/loop-system-e2e.test.js
jianf 377e02c943 fix(memory): make the loop actually promote real recall into system
`promoteSnapshotSections` accepted only ONE carrier spelling: `content: []`.
The plugin never emits that shape. `lib/index.js:932` always attaches exactly
one `{ type: 'text', text: injected }` block whose text IS `sections[0].text`,
so `promotedSection`'s `if (blocks.length > 0) return null` matched on every
real recall and the promotion was a no-op in production. Recall stayed in
user/message history — the exact defect 6d9ad62 claimed to fix.

6d9ad62's "surface recall as a system section instead of history" was never
true: the promotion it relies on was blocked by that content-non-empty check.
The unit tests could not see it because their `snapshotMessage()` helper built
`content: []` — a shape no producer emits — and the old P4 asserted "real
content blocks are NOT promoted", which is the opposite of reality.

It was confirmed today, from `lib/index.js:932`'s constant one-text-block
injection, and is fixed here.

The rule is now "move the text, don't rewrite it": a snapshot carrier with
content is promoted only when the rendered blocks equal the sections' text
under one of the two canonical joins (`''` glued, `'\n\n'` spaced), and the
promoted section text is the rendered content, so relocation is assertable.
Non-text blocks, mismatched text, and the stock runtime-context snapshot (whose
content carries a preamble the sections do not) pass through untouched.

Tests: S1-S7 plus real multi-block S2c/S2d pin the acceptance surface; every
negative case carries a positive control in the same decision, so the old
"content non-empty => reject" rule cannot satisfy it. P4 is split into P4a/b
(content == sections promotes; content != sections does not) with P4c proving
the split is decided by text, not by presence of content.
`test/loop-system-e2e.test.js` mounts the real HologramAgentLoop on real dsh
services, runs one turn, and asserts on the request the adapter received: the
recall is in the system prompt (E1) and in no non-system message body (E2),
carried by exactly one message (E2b).
2026-11-04 18:24:58 +08:00

195 lines
8.0 KiB
JavaScript

/**
* End-to-end proof: a recall block produced by the REAL injection shape reaches
* the SYSTEM prompt and does NOT reach any user/message content.
*
* Why this file exists next to `loop.test.js`
* -------------------------------------------
* `loop.test.js` asserts the pure decision rewrite (`promoteSnapshotSections`).
* That is necessary but not sufficient: the bug it fixes was that the rewrite
* never fired on the real injection shape, and a unit test can only catch that
* if it also encodes the real shape. This file closes the loop the other way —
* it mounts the actual `HologramAgentLoop` on real dsh services (`SessionStore`,
* `AgentRegistry`, `LlmRuntime`, `SystemPrompt`, `ToolRuntime`,
* `SessionProjectionRegistry`), drives one real turn (pre-step → request build →
* LLM call), and asserts against **the request object the adapter actually
* received**, not an intermediate decision.
*
* Two directions, both required:
* E1 positive — the recall text appears in the system prompt the adapter got.
* E2 negative — the recall text appears in NO message body the adapter got
* (every message, every block; not just the last one).
*
* Only E1 fails on the broken commit; E2 is already true there because the old
* code left recall in history. E2 is what this fix is *for*: it must stay true
* after the move, otherwise the promotion would duplicate recall rather than
* relocate it.
*
* No network: the "adapter" is a local `LlmAdapter` subclass that records the
* request and yields one text chunk.
*
* @module dsh-memory/test/loop-system-e2e
*/
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { pathToFileURL } from 'node:url'
import test from 'node:test'
import { Context } from '@deepseek-ai/cordis'
import { AgentRegistry } from '@deepseek-ai/dsh-agent'
import { LlmAdapter, LlmRuntime, createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionStore } from '@deepseek-ai/dsh-session'
import { SystemPrompt } from '@deepseek-ai/dsh-system-prompt'
import { ToolRuntime } from '@deepseek-ai/dsh-tools'
import { HologramAgentLoop } from '../lib/loop.js'
/**
* Load `SessionProjectionRegistry` the way the harness does.
*
* It is a peer of `dsh-agent-loop`, not of this package, so a bare specifier
* only resolves when the repo's `node_modules` happens to link it. Resolving
* from `dsh-agent-loop`'s own location is exactly what a real dsh process does;
* if neither route works there is no harness to run against, and the test skips
* loudly instead of asserting on a half-mounted app.
* @returns {Promise<Function|null>}
*/
async function loadProjectionRegistry() {
try {
return (await import('@deepseek-ai/dsh-session-projection')).SessionProjectionRegistry
} catch {
try {
const peerRequire = createRequire(import.meta.resolve('@deepseek-ai/dsh-agent-loop'))
return (await import(pathToFileURL(peerRequire.resolve('@deepseek-ai/dsh-session-projection')).href))
.SessionProjectionRegistry
} catch {
return null
}
}
}
/** Every text the model would read out of a message body. */
function bodyText(message) {
if (!Array.isArray(message?.content)) return ''
return message.content.map((block) => (typeof block?.text === 'string' ? block.text : '')).join('')
}
/** The system prompt text as the adapter received it. */
function systemText(request) {
return request.messages
.filter((message) => message.role === 'system')
.map((message) => bodyText(message))
.join('\n')
}
/** Every non-system message body, flattened, for the negative direction. */
function historyBlocks(request) {
return request.messages
.filter((message) => message.role !== 'system')
.map((message, index) => ({ index, role: message.role, kind: message.source?.kind, text: bodyText(message) }))
}
test('E1/E2: recall reaches the system prompt and no message body', async (t) => {
const SessionProjectionRegistry = await loadProjectionRegistry()
if (SessionProjectionRegistry === null) {
t.skip('@deepseek-ai/dsh-session-projection not resolvable from this install (no harness to mount)')
return
}
const RECALL = 'RECALLED-NOTE-4471: user prefers evidence over summaries.'
/** Records the request it is called with; yields one stop-finish text chunk. */
const captured = []
class CaptureAdapter extends LlmAdapter {
async *stream(request) {
captured.push(request)
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text: 'acknowledged' }
yield { type: 'block-end', index: 0, block: { type: 'text', text: 'acknowledged' } }
yield { type: 'finish', reason: 'stop' }
}
}
const ctx = new Context()
try {
// Cordis defers service publication by a microtask; each `new` below needs
// the previous one visible, so yield between mounts.
new SessionStore(ctx)
await new Promise((resolve) => setTimeout(resolve, 0))
new AgentRegistry(ctx)
new SystemPrompt(ctx, {})
new ToolRuntime(ctx)
new LlmRuntime(ctx)
new SessionProjectionRegistry(ctx)
await new Promise((resolve) => setTimeout(resolve, 0))
// The loop under test. Mount before any agent exists: `setFactory` runs in
// the constructor and the class refuses a second registration.
new HologramAgentLoop(ctx, { maxParallelToolCalls: 4, agents: [] })
await new Promise((resolve) => setTimeout(resolve, 0))
ctx.llm.registerAdapter(['capture'], new CaptureAdapter())
// The REAL injection shape, reproduced byte for byte from `lib/index.js:932`:
// one text block whose text IS the section text, `form: 'snapshot'`.
ctx.on(
'agent/pre-step',
async (payload, next) => {
const decision = await next()
if (decision.kind === 'reject' || payload.signal.aborted) return decision
return {
...decision,
messages: [
...decision.messages,
createUserMessage({
content: [{ type: 'text', text: RECALL }],
source: { kind: 'memory', form: 'snapshot', sections: [{ name: 'memory', text: RECALL }] },
}),
],
}
},
{ prepend: true },
)
const handle = await ctx.agents.create({
sessionId: 'loop-system-e2e',
agentOptions: { provider: 'capture', model: 'capture-1' },
})
const agent = handle.agent
agent.followup(
createUserMessage({ content: [{ type: 'text', text: 'what did we decide?' }], source: { kind: 'user' } }),
)
await agent.whenIdle()
assert.equal(captured.length, 1, `the adapter must be called exactly once; got ${captured.length} call(s)`)
const request = captured[0]
// ── E1 · positive: the recall is in the system prompt ────────────────
const system = systemText(request)
assert.ok(
system.includes(RECALL),
`E1: recall must appear in the system prompt the adapter received. system prompt was ${JSON.stringify(system)}`,
)
// ── E2 · negative: the recall is in NO message body ──────────────────
const history = historyBlocks(request)
const leaking = history.filter((entry) => entry.text.includes(RECALL))
assert.deepEqual(
leaking,
[],
`E2: recall must not appear in ANY non-system message; leaking messages: ${JSON.stringify(leaking)}`,
)
// E2b · the loop must not duplicate it either: exactly one system message
// may carry it, so a "promote but keep" bug cannot hide behind E1.
const carriers = request.messages.filter((message) => bodyText(message).includes(RECALL))
assert.equal(
carriers.length,
1,
`E2b: exactly one message may carry the recall (a move, not a copy); got ${carriers.length}: ${JSON.stringify(
carriers.map((message) => message.role),
)}`,
)
assert.equal(carriers[0].role, 'system', `E2b: the sole carrier must be the system message; got ${carriers[0].role}`)
} finally {
await ctx.fiber.dispose()
}
})