104 lines
4.6 KiB
JavaScript
104 lines
4.6 KiB
JavaScript
|
|
/**
|
|||
|
|
* 上下文注入用例(任务 A3.8)。
|
|||
|
|
*
|
|||
|
|
* 模拟「平台重启后继续同一会话」:新的引擎进程没有原生上下文,平台必须把
|
|||
|
|
* 「摘要 + 最近若干轮」注入进去,否则模型会失忆。
|
|||
|
|
* 同时锁住两点:同一运行时会话只注入一次;检查点与来源消息 id 落库可查。
|
|||
|
|
*/
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|||
|
|
import { tmpdir } from 'node:os';
|
|||
|
|
import { join } from 'node:path';
|
|||
|
|
import { test } from 'node:test';
|
|||
|
|
|
|||
|
|
/** 记录每轮收到的 prompt 文本,便于断言注入了什么 */
|
|||
|
|
function capturingRuntime() {
|
|||
|
|
const prompts = [];
|
|||
|
|
return {
|
|||
|
|
prompts,
|
|||
|
|
runtime: {
|
|||
|
|
name: 'capturing',
|
|||
|
|
toolExecution: 'platform',
|
|||
|
|
async createSession() {
|
|||
|
|
return {
|
|||
|
|
id: `cap-${Math.random().toString(36).slice(2)}`,
|
|||
|
|
toolExecution: 'platform',
|
|||
|
|
async *send(text) {
|
|||
|
|
prompts.push(text);
|
|||
|
|
yield { type: 'text', delta: '收到。' };
|
|||
|
|
yield { type: 'turn_end', reason: 'stop' };
|
|||
|
|
},
|
|||
|
|
resolveToolResult() {},
|
|||
|
|
abort() {},
|
|||
|
|
async dispose() {},
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function startServer(dbPath, runtime) {
|
|||
|
|
const { startServer: start } = await import('../server/dist/index.js');
|
|||
|
|
return start({ host: '127.0.0.1', port: 0, version: 'test', databasePath: dbPath, engine: 'fake', runtime });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const postJson = (base, path, body) =>
|
|||
|
|
fetch(`${base}${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|||
|
|
|
|||
|
|
async function send(server, sessionId, text, key) {
|
|||
|
|
const response = await postJson(server.url, `/v1/sessions/${sessionId}/messages`, { text, idempotencyKey: key });
|
|||
|
|
assert.equal(response.status, 202);
|
|||
|
|
// 等回合结束(事件流未订阅,这里直接轮询会话)
|
|||
|
|
for (let index = 0; index < 60; index += 1) {
|
|||
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|||
|
|
const detail = await (await fetch(`${server.url}/v1/sessions/${sessionId}`)).json();
|
|||
|
|
if (detail.messages.some((message) => message.role === 'assistant' && message.text === '收到。')) {
|
|||
|
|
const before = detail.messages.filter((message) => message.role === 'assistant' && message.text === '收到。').length;
|
|||
|
|
if (before >= 1) return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
throw new Error('回合未在预期时间内结束');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test('平台重启后把摘要与最近对话注入新引擎进程,且只注入一次', async () => {
|
|||
|
|
const dir = await mkdtemp(join(tmpdir(), 'aps-context-'));
|
|||
|
|
const dbPath = join(dir, 'aps.sqlite');
|
|||
|
|
try {
|
|||
|
|
// 第一个实例:建立会话与历史
|
|||
|
|
const first = capturingRuntime();
|
|||
|
|
const serverA = await startServer(dbPath, first.runtime);
|
|||
|
|
const created = await (await postJson(serverA.url, '/v1/sessions', { scope: 'project', projectId: 'p-default', title: '上下文用例' })).json();
|
|||
|
|
const sessionId = created.session.sessionId;
|
|||
|
|
await send(serverA, sessionId, '记住:车间换型时间是 25 分钟。', 'idem-ctx-0001');
|
|||
|
|
await send(serverA, sessionId, '再说一遍,重点是换型。', 'idem-ctx-0002');
|
|||
|
|
await serverA.close();
|
|||
|
|
|
|||
|
|
// 第二个实例:新引擎进程,平台必须补上下文
|
|||
|
|
const second = capturingRuntime();
|
|||
|
|
const serverB = await startServer(dbPath, second.runtime);
|
|||
|
|
try {
|
|||
|
|
await send(serverB, sessionId, '我刚才让你记住什么?', 'idem-ctx-0003');
|
|||
|
|
assert.equal(second.prompts.length, 1);
|
|||
|
|
const injected = second.prompts[0];
|
|||
|
|
assert.ok(injected.includes('【最近的对话】'), `重启后应注入最近对话:${injected.slice(0, 200)}`);
|
|||
|
|
assert.ok(injected.includes('车间换型时间是 25 分钟'), '注入内容应含重启前的原话');
|
|||
|
|
assert.ok(injected.includes('我刚才让你记住什么?'), '本轮用户输入也要在同一个 prompt 里');
|
|||
|
|
|
|||
|
|
// 同一运行时会话的第二轮不应该再注入(引擎自己有上下文)
|
|||
|
|
await send(serverB, sessionId, '再确认一下。', 'idem-ctx-0004');
|
|||
|
|
assert.equal(second.prompts.length, 2);
|
|||
|
|
assert.ok(!second.prompts[1].includes('【最近的对话】'), '第二轮不应重复注入上下文');
|
|||
|
|
|
|||
|
|
// 检查点落库:摘要来源与最后消息 id 可查
|
|||
|
|
const checkpoint = serverB.service.checkpoint(sessionId);
|
|||
|
|
assert.ok(checkpoint, '检查点应落库');
|
|||
|
|
assert.equal(typeof checkpoint.updatedAt, 'number');
|
|||
|
|
assert.ok(checkpoint.includedMessageIds.length > 0, '应记录保留过哪些消息');
|
|||
|
|
assert.ok(checkpoint.lastMessageId !== null);
|
|||
|
|
} finally {
|
|||
|
|
await serverB.close();
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
await rm(dir, { recursive: true, force: true });
|
|||
|
|
}
|
|||
|
|
});
|