fix: harden long conversation continuity
Memind CI / Test, build, and release guards (push) Successful in 1m28s
Memind CI / Test, build, and release guards (push) Successful in 1m28s
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user