fix: harden long conversation continuity
Memind CI / Test, build, and release guards (push) Successful in 1m28s

This commit is contained in:
john
2026-07-27 21:39:34 +08:00
parent 5ae166fc44
commit 48c61a3279
16 changed files with 1635 additions and 94 deletions
+150
View File
@@ -655,6 +655,90 @@ test('proxySessionEvents does not send JSON after SSE headers were sent', async
}
});
test('proxySessionEvents finalizes page delivery even when Finish billing fails', async () => {
let upstream;
try {
upstream = createServer((req, res) => {
if (req.method === 'GET' && req.url === '/sessions/session-1/events') {
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
res.end(
'data: '
+ JSON.stringify({
type: 'Finish',
request_id: 'request-1',
token_state: {
inputTokens: 1,
outputTokens: 1,
totalTokens: 2,
},
})
+ '\n\n',
);
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: `unexpected ${req.method} ${req.url}` }));
});
const upstreamPort = await listen(upstream);
const userAuth = createMemoryTestUserAuth(process.cwd());
userAuth.billSessionUsage = async () => {
throw new Error('billing unavailable');
};
const proxy = createTkmindProxy({
apiTarget: `http://127.0.0.1:${upstreamPort}`,
apiSecret: 'test-secret',
userAuth,
});
const req = new EventEmitter();
req.currentUser = { id: 'user-1', username: 'john' };
req.get = () => '';
req.once = req.once.bind(req);
req.off = req.off.bind(req);
const res = new EventEmitter();
res.headersSent = false;
res.writableEnded = false;
res.status = () => res;
res.setHeader = () => {};
res.flushHeaders = () => {
res.headersSent = true;
};
res.write = () => true;
res.end = () => {
res.writableEnded = true;
};
let resolveAfterFinish;
const afterFinishCalled = new Promise((resolve) => {
resolveAfterFinish = resolve;
});
await proxy.proxySessionEvents(
req,
res,
'session-1',
{
async onAfterFinish() {
resolveAfterFinish();
},
},
);
await Promise.race([
afterFinishCalled,
new Promise((_, reject) =>
setTimeout(
() => reject(new Error('page delivery finalization was skipped')),
100,
),
),
]);
} finally {
await closeServer(upstream);
}
});
test('proxySessionEvents attaches session taxonomy when flag enabled', async () => {
const previous = process.env.MEMIND_SSE_EVENT_TAXONOMY;
process.env.MEMIND_SSE_EVENT_TAXONOMY = '1';
@@ -1304,6 +1388,72 @@ test('submitSessionReplyForUser requests a fresh session when Goose cannot updat
}, { conversation });
});
test('compactSessionConversationForUser clears oversized Goose history and returns it for recovery context', async () => {
const conversation = [
{ role: 'user', content: [{ type: 'text', text: '创建问卷页面' }] },
{ role: 'assistant', content: [{ type: 'text', text: '页面已创建' }] },
{ role: 'user', content: [{ type: 'text', text: '继续修改第 20 轮' }] },
];
await withFakeGoosedSession(async ({ apiTarget, workingDir, updateBodies }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: {
...createMemoryTestUserAuth(workingDir),
async ownsSession() {
return true;
},
},
});
const result = await proxy.compactSessionConversationForUser(
'user-1',
'session-1',
{ messageThreshold: 3, charThreshold: 1_000_000 },
);
assert.equal(result.compacted, true);
assert.equal(result.messageCount, 3);
assert.deepEqual(result.conversation, conversation);
assert.equal(updateBodies.length, 1);
assert.deepEqual(updateBodies[0].conversation, []);
}, { conversation, allowConversationUpdate: true });
});
test('compactSessionConversationForUser requests transparent rotation when Goose cannot update history', async () => {
const conversation = [
{ role: 'user', content: [{ type: 'text', text: '第 26 轮继续修改' }] },
{ role: 'assistant', content: [{ type: 'text', text: 'DURABILITY-26' }] },
];
await withFakeGoosedSession(async ({ apiTarget, workingDir }) => {
const proxy = createTkmindProxy({
apiTarget,
apiSecret: 'test-secret',
userAuth: {
...createMemoryTestUserAuth(workingDir),
async ownsSession() {
return true;
},
},
});
await assert.rejects(
proxy.compactSessionConversationForUser(
'user-1',
'session-1',
{ messageThreshold: 2, charThreshold: 1_000_000 },
),
(error) => {
assert.equal(error.code, 'SESSION_CONTEXT_FRESH_SESSION_REQUIRED');
assert.deepEqual(error.conversation, conversation);
return true;
},
);
}, { conversation, allowConversationUpdate: false });
});
test('submitSessionReplyForUser fails closed when historical image scrub is unsupported', async () => {
await withFakeGoosedSession(async ({ apiTarget, workingDir, replyBodies }) => {
const proxy = createTkmindProxy({