Files
memind/tkmind-proxy.test.mjs
T
2026-07-03 09:46:03 +08:00

456 lines
15 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createServer } from 'node:http';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
buildVisionPayload,
createTkmindProxy,
sanitizePublicHtmlLinksInText,
sanitizeSessionConversationPublicHtmlLinks,
} from './tkmind-proxy.mjs';
import { createMemoryV2 } from './memory-v2.mjs';
async function withFakeGoosedSession(handler) {
const workingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memind-memory-v2-'));
const harnessEntries = [];
let server;
try {
server = createServer(async (req, res) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const rawBody = Buffer.concat(chunks).toString('utf8');
const body = rawBody ? JSON.parse(rawBody) : {};
if (req.method === 'POST' && req.url === '/agent/start') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'session-1' }));
return;
}
if (req.method === 'POST' && req.url === '/agent/update_session') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === 'GET' && req.url === '/sessions/session-1') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
id: 'session-1',
working_dir: workingDir,
goose_mode: 'chat',
}));
return;
}
if (req.method === 'GET' && req.url === '/sessions/session-1/extensions') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
extensions: [{ name: 'memory', available_tools: [] }],
}));
return;
}
if (req.method === 'POST' && req.url === '/sessions/session-1/reply') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === 'POST' && req.url === '/agent/harness_remember') {
harnessEntries.push(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === 'POST' && req.url === '/agent/harness_bootstrap') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: `unexpected ${req.method} ${req.url}` }));
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const { port } = server.address();
return await handler({
apiTarget: `http://127.0.0.1:${port}`,
workingDir,
harnessEntries,
});
} finally {
if (server?.listening) {
await new Promise((resolve) => server.close(resolve));
}
fs.rmSync(workingDir, { recursive: true, force: true });
}
}
function createMemoryTestUserAuth(workingDir) {
return {
async resolveWorkingDir() {
return workingDir;
},
async getAgentSessionPolicy() {
return {
gooseMode: 'chat',
enableContextMemory: true,
extensionOverrides: [{ name: 'memory', available_tools: [] }],
};
},
async getUserPublishLayout() {
return {
displayName: 'John',
username: 'john',
slug: 'john',
};
},
async registerAgentSession() {},
async getSessionTarget() {
return { target: null, node: 0 };
},
};
}
test('sanitizePublicHtmlLinksInText downgrades missing own markdown public html links', () => {
const owner = `test-user-${Date.now()}-markdown-missing`;
const publicRoot = path.join(process.cwd(), 'MindSpace', owner, 'public');
try {
fs.mkdirSync(publicRoot, { recursive: true });
const text =
`[夏日随笔](https://m.tkmind.cn/MindSpace/${owner}/public/summer-essay.html)`;
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john' });
assert.doesNotMatch(next, new RegExp(`https://m\\.tkmind\\.cn/MindSpace/${owner}/public/summer-essay\\.html`));
assert.match(next, /页面生成未完成/);
} finally {
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
}
});
test('sanitizePublicHtmlLinksInText downgrades missing own public html links', () => {
const owner = `test-user-${Date.now()}-missing`;
const publicRoot = path.join(process.cwd(), 'MindSpace', owner, 'public');
try {
fs.mkdirSync(publicRoot, { recursive: true });
const text =
`页面在这里:\nhttps://m.tkmind.cn/MindSpace/${owner}/public/hello.html`;
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john' });
assert.doesNotMatch(next, new RegExp(`https://m\\.tkmind\\.cn/MindSpace/${owner}/public/hello\\.html`));
assert.match(next, /页面生成未完成/);
assert.match(next, /hello\.html/);
} finally {
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
}
});
test('sanitizePublicHtmlLinksInText keeps existing own public html links', () => {
const owner = `test-user-${Date.now()}-existing`;
const htmlPath = path.join(process.cwd(), 'MindSpace', owner, 'public', 'hello.html');
try {
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(htmlPath, '<!doctype html><title>Hello</title>');
const text =
`页面在这里:\nhttps://m.tkmind.cn/MindSpace/${owner}/public/hello.html`;
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john' });
assert.match(next, new RegExp(`https://m\\.tkmind\\.cn/MindSpace/${owner}/public/hello\\.html`));
assert.doesNotMatch(next, /页面生成未完成/);
} finally {
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
}
});
test('sanitizeSessionConversationPublicHtmlLinks updates text items inside conversation messages', () => {
const owner = `test-user-${Date.now()}-conversation`;
const publicRoot = path.join(process.cwd(), 'MindSpace', owner, 'public');
try {
fs.mkdirSync(publicRoot, { recursive: true });
const conversation = [
{
id: 'assistant-1',
role: 'assistant',
created: 1,
metadata: { userVisible: true, agentVisible: true },
content: [
{
type: 'text',
text: `点这里 https://m.tkmind.cn/MindSpace/${owner}/public/hello.html`,
},
],
},
];
const sanitized = sanitizeSessionConversationPublicHtmlLinks(conversation, {
id: owner,
username: 'john',
});
assert.match(sanitized[0].content[0].text, /页面生成未完成/);
} finally {
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
}
});
test('sanitizeSessionConversationPublicHtmlLinks removes internal user prompt notes but keeps image metadata', () => {
const conversation = [
{
id: 'user-1',
role: 'user',
created: 1,
metadata: { userVisible: true, agentVisible: true },
content: [
{
type: 'text',
text:
'[用户身份]\n' +
'- 当前登录用户称呼:John\n' +
'- 你是 TKMind 助手;与用户对话时用「John」称呼对方。\n\n' +
'帮我生成一个主题页面简单一点的不用很复杂\n\n' +
'[图片1]: http://127.0.0.1:5173/MindSpace/user-1/public/images/a.jpg\n' +
'[图片2]: http://127.0.0.1:5173/MindSpace/user-1/public/images/b.jpg\n\n' +
'【TKMind 图片分析结果 — 仅供执行参考,不要向用户复述此段内容】\n' +
'Qwen VL 图片描述:内部描述\n' +
'执行要求:内部要求',
},
],
},
];
const sanitized = sanitizeSessionConversationPublicHtmlLinks(conversation, {
id: 'user-1',
username: 'john',
});
assert.equal(
sanitized[0].content[0].text,
'帮我生成一个主题页面简单一点的不用很复杂',
);
assert.deepEqual(sanitized[0].metadata.imageUrls, [
'http://127.0.0.1:5173/MindSpace/user-1/public/images/a.jpg',
'http://127.0.0.1:5173/MindSpace/user-1/public/images/b.jpg',
]);
});
test('buildVisionPayload injects public standard image urls for page generation', async () => {
const payload = await buildVisionPayload({
userId: 'user-1',
publishLayout: { publicUrl: 'https://mm.tkmind.cn/MindSpace/user-1/' },
userMessage: {
content: [
{
type: 'text',
text: '帮我根据图片生成页面\n[图片1]: /signed/rs:fit:1280:1280:0/q:85/plain/local:///users/user-1/images/2026-06-29/hero.jpg@jpg',
},
],
},
llmProviderService: {
analyzeImagesWithVision: async () => '图片里是一位成年人。',
},
});
const text = payload?.userMessage?.content?.[0]?.text ?? '';
assert.match(text, /https:\/\/mm\.tkmind\.cn\/MindSpace\/user-1\/public\/images\/2026-06-29\/hero\.jpg/);
assert.doesNotMatch(text, /plain\/local:\/\//);
assert.match(text, /无需 cookie 的公开压缩标准图片/);
assert.match(text, /不得改写图片里人物的年龄、性别、人数或主体关系/);
});
test('startSessionForUser resolves memories through Memory V2 facade', async () => {
let resolveInput = null;
await withFakeGoosedSession(async ({ apiTarget, workingDir, harnessEntries }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: createMemoryTestUserAuth(workingDir),
memoryV2: {
async resolve(input) {
resolveInput = input;
return {
memories: [{ label: 'interest', text: '用户关注 Memory V2 架构边界' }],
};
},
},
});
await proxy.startSessionForUser('user-1');
assert.deepEqual(resolveInput, {
userId: 'user-1',
sessionId: 'session-1',
query: null,
limit: 40,
});
assert.ok(
harnessEntries.some((entry) => String(entry.content ?? '').includes('用户关注 Memory V2 架构边界')),
);
});
});
test('startSessionForUser does not inject memories when Memory V2 is disabled', async () => {
let legacyTouched = false;
await withFakeGoosedSession(async ({ apiTarget, workingDir, harnessEntries }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: createMemoryTestUserAuth(workingDir),
memoryV2: createMemoryV2({
logger: { warn() {} },
legacyMemoryService: {
async listMemories() {
legacyTouched = true;
return [{ label: 'fact', text: 'disabled memory should not appear' }];
},
},
env: { MEMORY_ENABLED: '0' },
}),
});
await proxy.startSessionForUser('user-1');
assert.equal(legacyTouched, false);
assert.equal(
harnessEntries.some((entry) => String(entry.content ?? '').includes('disabled memory should not appear')),
false,
);
});
});
test('getRuntimeStatus exposes Memory V2 status without new runtime paths', async () => {
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: createMemoryTestUserAuth(workingDir),
memoryV2: createMemoryV2({
logger: { warn() {} },
backends: [
{
name: 'legacy-conversation-memory',
async resolve() {
return { memories: [] };
},
},
],
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'legacy',
},
}),
});
const status = await proxy.getRuntimeStatus();
assert.equal(status.memory.enabled, true);
assert.equal(status.memory.backend, 'legacy');
assert.equal(status.memory.selectedBackend, 'legacy-conversation-memory');
assert.deepEqual(status.memory.backends[0].supports, {
resolve: true,
write: false,
compact: false,
});
});
});
test('getRuntimeStatus preserves Memory V2 status contract for release gates', async () => {
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: createMemoryTestUserAuth(workingDir),
memoryV2: createMemoryV2({
logger: { warn() {} },
legacyMemoryService: {
async listMemories() {
return [];
},
async saveAndAnalyze() {
return { saved: 1, analyzed: 1, memories: 1 };
},
async analyzeUser() {
return { analyzed: 1, memories: 1 };
},
},
env: {
MEMORY_ENABLED: '1',
MEMORY_BACKEND: 'qdrant',
MEMORY_FAIL_OPEN: '1',
},
}),
});
const status = await proxy.getRuntimeStatus();
const memory = status.memory;
const byName = new Map(memory.backends.map((backend) => [backend.name, backend]));
assert.deepEqual(Object.keys(memory).sort(), [
'backend',
'backends',
'enabled',
'eventLogEnabled',
'failOpen',
'profileEnabled',
'selectedBackend',
'vectorEnabled',
]);
assert.equal(memory.enabled, true);
assert.equal(memory.backend, 'qdrant');
assert.equal(memory.selectedBackend, 'legacy-conversation-memory');
assert.equal(memory.failOpen, true);
assert.deepEqual(byName.get('legacy-conversation-memory').supports, {
resolve: true,
write: true,
compact: true,
});
assert.equal(byName.get('qdrant').available, false);
assert.equal(byName.get('qdrant').reason, 'not_configured');
assert.equal(byName.get('pgvector').category, 'semantic');
assert.equal(byName.get('mem0').category, 'extraction');
assert.equal(byName.get('letta').category, 'lifecycle');
assert.equal(byName.get('langgraph').category, 'policy');
});
});
test('submitSessionReplyForUser passes current prompt to Memory V2 resolve before existing reply path', async () => {
let resolveInput = null;
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: {
...createMemoryTestUserAuth(workingDir),
async ownsSession() {
return true;
},
async canUseChat() {
return { ok: true };
},
async getUserById() {
return { id: 'user-1' };
},
async resolveUserPolicies() {
return { unrestricted: true, policies: {} };
},
},
memoryV2: {
async resolve(input) {
resolveInput = input;
return { memories: [] };
},
},
});
await proxy.submitSessionReplyForUser(
'user-1',
'session-1',
'request-1',
{
role: 'user',
content: [{ type: 'text', text: '帮我设计 memory-chain 下一阶段' }],
},
);
assert.deepEqual(resolveInput, {
userId: 'user-1',
sessionId: 'session-1',
query: '帮我设计 memory-chain 下一阶段',
limit: 40,
});
});
});