Files
memind/server/portal-user-memory-routes.test.mjs

422 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalUserMemoryRoutes } from './portal-user-memory-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
return {
routes,
get(path, handler) {
routes.set(`GET ${path}`, handler);
},
post(path, handler) {
routes.set(`POST ${path}`, handler);
},
delete(path, handler) {
routes.set(`DELETE ${path}`, handler);
},
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
};
}
function createRequest(overrides = {}) {
return {
body: {},
query: {},
params: {},
currentUser: { id: 'user-1' },
...overrides,
};
}
function enabledMemoryV2(overrides = {}) {
return {
getStatus: async () => ({ enabled: true }),
...overrides,
};
}
function createDependencies(overrides = {}) {
return {
getMemoryV2: () => enabledMemoryV2(),
getTkmindProxy: () => ({}),
ensureUserMemoryCapability: async () => ({ unrestricted: true }),
ownsAgentSession: async () => true,
loadUserVisibleConversation: async () => [],
syncUserMemoriesIntoSession: async () => true,
resolveUserMemoryItems: async () => [],
...overrides,
};
}
test('User Memory module preserves route inventory and order', () => {
const api = createRouterRecorder();
attachPortalUserMemoryRoutes(api);
assert.deepEqual([...api.routes.keys()], [
'POST /user-memory/v1/remember-recent',
'POST /user-memory/v1/sync',
'GET /user-memory/v1/items',
'DELETE /user-memory/v1/items/:memoryId',
]);
});
test('remember-recent preserves memory, proxy, and capability gates', async () => {
const disabledApi = createRouterRecorder();
attachPortalUserMemoryRoutes(disabledApi, {
getMemoryV2: () => ({
async getStatus() {
throw new Error('status failed');
},
}),
});
const disabledRes = createResponseRecorder();
await disabledApi.routes.get('POST /user-memory/v1/remember-recent')(
createRequest(),
disabledRes,
);
assert.equal(disabledRes.statusCode, 503);
assert.deepEqual(disabledRes.body, { message: '长期记忆功能未启用' });
const proxyApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
proxyApi,
createDependencies({ getTkmindProxy: () => null }),
);
const proxyRes = createResponseRecorder();
await proxyApi.routes.get('POST /user-memory/v1/remember-recent')(
createRequest(),
proxyRes,
);
assert.equal(proxyRes.statusCode, 503);
assert.deepEqual(proxyRes.body, { message: '会话代理尚未就绪' });
let ownershipChecked = false;
const capabilityApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
capabilityApi,
createDependencies({
ensureUserMemoryCapability: async (_req, res) => {
res.status(403).json({ message: 'memory denied' });
return null;
},
ownsAgentSession: async () => {
ownershipChecked = true;
return true;
},
}),
);
const capabilityRes = createResponseRecorder();
await capabilityApi.routes.get('POST /user-memory/v1/remember-recent')(
createRequest({ body: { sessionId: 'session-1' } }),
capabilityRes,
);
assert.equal(capabilityRes.statusCode, 403);
assert.equal(ownershipChecked, false);
});
test('remember-recent preserves session validation and ownership gate', async () => {
const missingApi = createRouterRecorder();
attachPortalUserMemoryRoutes(missingApi, createDependencies());
const missingRes = createResponseRecorder();
await missingApi.routes.get('POST /user-memory/v1/remember-recent')(
createRequest({ body: { sessionId: ' ' } }),
missingRes,
);
assert.equal(missingRes.statusCode, 400);
assert.deepEqual(missingRes.body, { message: '缺少 sessionId' });
let ownershipInput = null;
const deniedApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
deniedApi,
createDependencies({
ownsAgentSession: async (...args) => {
ownershipInput = args;
return false;
},
}),
);
const deniedRes = createResponseRecorder();
await deniedApi.routes.get('POST /user-memory/v1/remember-recent')(
createRequest({ body: { sessionId: ' session-1 ' } }),
deniedRes,
);
assert.deepEqual(ownershipInput, ['user-1', 'session-1']);
assert.equal(deniedRes.statusCode, 403);
assert.deepEqual(deniedRes.body, { message: '无权访问该会话' });
});
test('remember-recent preserves write, sync, resolve order and response projection', async () => {
const calls = [];
const messages = [{ role: 'user', content: 'remember this' }];
const api = createRouterRecorder();
attachPortalUserMemoryRoutes(
api,
createDependencies({
getMemoryV2: () =>
enabledMemoryV2({
async write(input) {
calls.push({ kind: 'write', input });
return { analyzed: 3, memories: 2 };
},
}),
loadUserVisibleConversation: async (...args) => {
calls.push({ kind: 'load', args });
return messages;
},
syncUserMemoriesIntoSession: async (...args) => {
calls.push({ kind: 'sync', args });
return true;
},
resolveUserMemoryItems: async (...args) => {
calls.push({ kind: 'resolve', args });
return [{ id: 'm1' }, { id: 'm2' }, { id: 'm3' }];
},
}),
);
const res = createResponseRecorder();
await api.routes.get('POST /user-memory/v1/remember-recent')(
createRequest({ body: { sessionId: 'session-1' } }),
res,
);
assert.deepEqual(calls, [
{ kind: 'load', args: ['session-1', 'user-1'] },
{
kind: 'write',
input: { userId: 'user-1', sessionId: 'session-1', messages },
},
{ kind: 'sync', args: ['user-1', 'session-1'] },
{
kind: 'resolve',
args: ['user-1', { sessionId: 'session-1', limit: 200 }],
},
]);
assert.deepEqual(res.body, {
ok: true,
analyzed: 3,
memories: 2,
totalMemories: 3,
syncedToSession: true,
});
});
test('remember-recent preserves operation error response', async () => {
const api = createRouterRecorder();
attachPortalUserMemoryRoutes(
api,
createDependencies({
loadUserVisibleConversation: async () => {
throw new Error('conversation unavailable');
},
}),
);
const res = createResponseRecorder();
await api.routes.get('POST /user-memory/v1/remember-recent')(
createRequest({ body: { sessionId: 'session-1' } }),
res,
);
assert.equal(res.statusCode, 500);
assert.deepEqual(res.body, { message: 'conversation unavailable' });
});
test('sync preserves compact flow, defaults, and operation errors', async () => {
const calls = [];
const api = createRouterRecorder();
attachPortalUserMemoryRoutes(
api,
createDependencies({
getMemoryV2: () =>
enabledMemoryV2({
async compact(input) {
calls.push({ kind: 'compact', input });
return {};
},
}),
syncUserMemoriesIntoSession: async (...args) => {
calls.push({ kind: 'sync', args });
return false;
},
resolveUserMemoryItems: async (...args) => {
calls.push({ kind: 'resolve', args });
return [{ id: 'm1' }];
},
}),
);
const res = createResponseRecorder();
await api.routes.get('POST /user-memory/v1/sync')(
createRequest({ body: { sessionId: 'session-2' } }),
res,
);
assert.deepEqual(calls, [
{
kind: 'compact',
input: { userId: 'user-1', sessionId: 'session-2' },
},
{ kind: 'sync', args: ['user-1', 'session-2'] },
{
kind: 'resolve',
args: ['user-1', { sessionId: 'session-2', limit: 200 }],
},
]);
assert.deepEqual(res.body, {
ok: true,
analyzed: 0,
memories: 0,
totalMemories: 1,
syncedToSession: false,
});
const failureApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
failureApi,
createDependencies({
getMemoryV2: () =>
enabledMemoryV2({
async compact() {
throw 'compact failed';
},
}),
}),
);
const failureRes = createResponseRecorder();
await failureApi.routes.get('POST /user-memory/v1/sync')(
createRequest({ body: { sessionId: 'session-2' } }),
failureRes,
);
assert.equal(failureRes.statusCode, 500);
assert.deepEqual(failureRes.body, { message: '刷新长期记忆失败' });
});
test('items preserves query forwarding, empty fallback, and read errors', async () => {
let received = null;
const api = createRouterRecorder();
attachPortalUserMemoryRoutes(
api,
createDependencies({
getMemoryV2: () => ({
async listMemories(input) {
received = input;
return [{ id: 'memory-1' }];
},
}),
}),
);
const res = createResponseRecorder();
await api.routes.get('GET /user-memory/v1/items')(
createRequest({ query: { status: 'forgotten', limit: '10', offset: '2' } }),
res,
);
assert.deepEqual(received, {
userId: 'user-1',
status: 'forgotten',
limit: '10',
offset: '2',
});
assert.deepEqual(res.body, { ok: true, items: [{ id: 'memory-1' }] });
const fallbackApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
fallbackApi,
createDependencies({ getMemoryV2: () => ({}) }),
);
const fallbackRes = createResponseRecorder();
await fallbackApi.routes.get('GET /user-memory/v1/items')(
createRequest(),
fallbackRes,
);
assert.deepEqual(fallbackRes.body, { ok: true, items: [] });
const failureApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
failureApi,
createDependencies({
getMemoryV2: () => ({
async listMemories() {
throw new Error('read failed');
},
}),
}),
);
const failureRes = createResponseRecorder();
await failureApi.routes.get('GET /user-memory/v1/items')(
createRequest(),
failureRes,
);
assert.equal(failureRes.statusCode, 500);
assert.deepEqual(failureRes.body, { message: 'read failed' });
});
test('delete preserves forwarding, unavailable fallback, success, and errors', async () => {
let received = null;
const skippedApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
skippedApi,
createDependencies({ getMemoryV2: () => ({}) }),
);
const skippedRes = createResponseRecorder();
await skippedApi.routes.get('DELETE /user-memory/v1/items/:memoryId')(
createRequest({ params: { memoryId: 'memory-1' } }),
skippedRes,
);
assert.equal(skippedRes.statusCode, 409);
assert.deepEqual(skippedRes.body, {
ok: false,
skipped: true,
reason: 'unavailable',
});
const successApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
successApi,
createDependencies({
getMemoryV2: () => ({
async forgetMemory(input) {
received = input;
return { ok: true };
},
}),
}),
);
const successRes = createResponseRecorder();
await successApi.routes.get('DELETE /user-memory/v1/items/:memoryId')(
createRequest({ params: { memoryId: 'memory-2' } }),
successRes,
);
assert.deepEqual(received, { userId: 'user-1', memoryId: 'memory-2' });
assert.deepEqual(successRes.body, { ok: true });
const failureApi = createRouterRecorder();
attachPortalUserMemoryRoutes(
failureApi,
createDependencies({
getMemoryV2: () => ({
async forgetMemory() {
throw new Error('forget failed');
},
}),
}),
);
const failureRes = createResponseRecorder();
await failureApi.routes.get('DELETE /user-memory/v1/items/:memoryId')(
createRequest({ params: { memoryId: 'memory-3' } }),
failureRes,
);
assert.equal(failureRes.statusCode, 500);
assert.deepEqual(failureRes.body, { message: 'forget failed' });
});