refactor: modularize portal server and harden session recovery

This commit is contained in:
john
2026-07-24 18:39:24 +08:00
parent 1d291fd528
commit 785731d98d
109 changed files with 29185 additions and 5836 deletions
+148 -2
View File
@@ -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) {
@@ -881,6 +894,7 @@ test('getRuntimeStatus preserves Memory V2 status contract for release gates', a
'eventLogEnabled',
'failOpen',
'profileEnabled',
'runtimeControl',
'selectedBackend',
'vectorEnabled',
]);
@@ -940,6 +954,138 @@ test('submitSessionReplyForUser adds goose metadata visibility flags before repl
});
});
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({