merge: server architecture modularization
This commit is contained in:
+184
-2
@@ -13,10 +13,15 @@ import {
|
||||
} from './tkmind-proxy.mjs';
|
||||
import { createMemoryV2 } from './memory-v2.mjs';
|
||||
|
||||
async function withFakeGoosedSession(handler, { conversation = [] } = {}) {
|
||||
async function withFakeGoosedSession(
|
||||
handler,
|
||||
{ conversation = [], allowConversationUpdate = false } = {},
|
||||
) {
|
||||
const workingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memind-memory-v2-'));
|
||||
const harnessEntries = [];
|
||||
const replyBodies = [];
|
||||
const updateBodies = [];
|
||||
let activeConversation = conversation;
|
||||
let server;
|
||||
|
||||
try {
|
||||
@@ -42,11 +47,18 @@ async function withFakeGoosedSession(handler, { conversation = [] } = {}) {
|
||||
id: 'session-1',
|
||||
working_dir: workingDir,
|
||||
goose_mode: 'chat',
|
||||
conversation,
|
||||
conversation: activeConversation,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (req.method === 'PUT' && req.url === '/sessions/session-1') {
|
||||
if (allowConversationUpdate) {
|
||||
updateBodies.push(body);
|
||||
activeConversation = body.conversation;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: 'method not allowed' }));
|
||||
return;
|
||||
@@ -86,6 +98,7 @@ async function withFakeGoosedSession(handler, { conversation = [] } = {}) {
|
||||
workingDir,
|
||||
harnessEntries,
|
||||
replyBodies,
|
||||
updateBodies,
|
||||
});
|
||||
} finally {
|
||||
if (server?.listening) {
|
||||
@@ -198,6 +211,42 @@ test('sanitizePublicHtmlLinksInText keeps existing own public html links', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('sanitizePublicHtmlLinksInText finds pages in the workspace beside H5_USERS_ROOT', () => {
|
||||
const owner = `test-user-${Date.now()}-shared-workspace`;
|
||||
const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'memind-shared-workspace-'));
|
||||
const previousUsersRoot = process.env.H5_USERS_ROOT;
|
||||
const htmlPath = path.join(runtimeRoot, 'MindSpace', owner, 'public', 'poem.html');
|
||||
process.env.H5_USERS_ROOT = path.join(runtimeRoot, 'users');
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
fs.writeFileSync(htmlPath, '<!doctype html><title>Poem</title>');
|
||||
const text =
|
||||
`[诗词页面](https://m.tkmind.cn/MindSpace/${owner}/public/poem.html)`;
|
||||
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john' });
|
||||
assert.match(next, new RegExp(`/MindSpace/${owner}/public/poem\\.html`));
|
||||
assert.doesNotMatch(next, /页面生成未完成/);
|
||||
} finally {
|
||||
if (previousUsersRoot == null) delete process.env.H5_USERS_ROOT;
|
||||
else process.env.H5_USERS_ROOT = previousUsersRoot;
|
||||
fs.rmSync(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('sanitizePublicHtmlLinksInText restores a previously suppressed link after the page appears', () => {
|
||||
const owner = `test-user-${Date.now()}-restored-link`;
|
||||
const htmlPath = path.join(process.cwd(), 'MindSpace', owner, 'public', 'poem.html');
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
fs.writeFileSync(htmlPath, '<!doctype html><title>Poem</title>');
|
||||
const text = '页面链接:\n(页面生成未完成,已阻止显示失效链接:poem.html。)';
|
||||
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john' });
|
||||
assert.match(next, new RegExp(`\\[poem\\.html\\]\\([^)]*/MindSpace/${owner}/public/poem\\.html\\)`));
|
||||
assert.doesNotMatch(next, /页面生成未完成/);
|
||||
} finally {
|
||||
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('sanitizePublicHtmlLinksInText canonicalizes wrong MindSpace public hosts', () => {
|
||||
const owner = `test-user-${Date.now()}-canonical-host`;
|
||||
const previousBase = process.env.H5_PUBLIC_BASE_URL;
|
||||
@@ -845,6 +894,7 @@ test('getRuntimeStatus preserves Memory V2 status contract for release gates', a
|
||||
'eventLogEnabled',
|
||||
'failOpen',
|
||||
'profileEnabled',
|
||||
'runtimeControl',
|
||||
'selectedBackend',
|
||||
'vectorEnabled',
|
||||
]);
|
||||
@@ -991,6 +1041,138 @@ test('submitSessionReplyForUser fails open when disclosure policy evaluation thr
|
||||
});
|
||||
});
|
||||
|
||||
test('submitSessionReplyForUser repairs poisoned tool history before sending the next turn', async () => {
|
||||
const conversation = [
|
||||
{ role: 'user', content: [{ type: 'text', text: '生成页面' }] },
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'toolRequest',
|
||||
id: 'call-a',
|
||||
toolCall: { status: 'success', value: { name: 'write_file', arguments: {} } },
|
||||
}],
|
||||
},
|
||||
{ role: 'assistant', content: [{ type: 'text', text: '继续' }] },
|
||||
{
|
||||
role: 'user',
|
||||
content: [{
|
||||
type: 'toolResponse',
|
||||
id: 'call-a',
|
||||
toolResult: {
|
||||
status: 'success',
|
||||
value: { content: [{ type: 'text', text: 'ok' }], isError: false },
|
||||
},
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies, updateBodies }) => {
|
||||
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: {} };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await proxy.submitSessionReplyForUser(
|
||||
'user-1',
|
||||
'session-1',
|
||||
'request-after-tool-repair',
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '继续生成' }],
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(updateBodies.length, 1);
|
||||
assert.deepEqual(
|
||||
updateBodies[0].conversation.map(
|
||||
(message) => message.content[0]?.id ?? message.content[0]?.text,
|
||||
),
|
||||
['生成页面', 'call-a', 'call-a', '继续'],
|
||||
);
|
||||
assert.equal(replyBodies.length, 1);
|
||||
}, { conversation, allowConversationUpdate: true });
|
||||
});
|
||||
|
||||
test('submitSessionReplyForUser requests a fresh session when Goose cannot update history', async () => {
|
||||
const conversation = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'toolRequest',
|
||||
id: 'call-a',
|
||||
toolCall: { status: 'success', value: { name: 'write_file', arguments: {} } },
|
||||
}],
|
||||
},
|
||||
{ role: 'assistant', content: [{ type: 'text', text: '继续' }] },
|
||||
{
|
||||
role: 'user',
|
||||
content: [{
|
||||
type: 'toolResponse',
|
||||
id: 'call-a',
|
||||
toolResult: {
|
||||
status: 'success',
|
||||
value: { content: [{ type: 'text', text: 'ok' }], isError: false },
|
||||
},
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => {
|
||||
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: {} };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
proxy.submitSessionReplyForUser(
|
||||
'user-1',
|
||||
'session-1',
|
||||
'request-needs-fresh-session',
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '继续生成' }],
|
||||
},
|
||||
),
|
||||
(error) => {
|
||||
assert.equal(error.code, 'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED');
|
||||
assert.equal(error.repairedConversation.length, 3);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
assert.equal(replyBodies.length, 0);
|
||||
}, { conversation });
|
||||
});
|
||||
|
||||
test('submitSessionReplyForUser fails closed when historical image scrub is unsupported', async () => {
|
||||
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => {
|
||||
const proxy = createTkmindProxy({
|
||||
|
||||
Reference in New Issue
Block a user