修复完整会话历史与长气泡交互并更新 CentOS 7 发布包

提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
shiyue
2026-10-10 23:40:16 +08:00
parent 747771a048
commit 69b6db0f5b
71 changed files with 9536 additions and 503 deletions

View File

@@ -0,0 +1,347 @@
#!/usr/bin/env node
'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
// 直接读取服务端函数,避免单测中的业务副本与实际实现逐渐偏离。
const serverPath = path.join(__dirname, '..', 'server.js');
const serverSource = fs.readFileSync(serverPath, 'utf8');
const functionNames = [
'codexAppAssistantMessageIdForEntry',
'ensureCodexAppAssistantMessageId',
'buildCodexAppResumePayload',
'retireCodexAppRuntimeTurn',
'handleMessage',
'handleCodexAppMessage',
'handleCodexAppTurnComplete',
'isCodexAppNoActiveTurnError',
'rollbackPersistedCodexAppSteerMessage',
'reorderStaleCodexAppTurnMessages',
'handleCodexAppSteerMessage',
];
function extractFunction(name) {
const match = new RegExp(`^function ${name}\\(`, 'm').exec(serverSource);
assert.ok(match, `服务端应存在实际函数:${name}`);
const closingLine = /\n\}/g;
closingLine.lastIndex = match.index;
let end;
while ((end = closingLine.exec(serverSource))) {
const candidate = serverSource.slice(match.index, end.index + 2);
try {
// 编译成单一表达式,防止误带相邻函数或全局初始化代码。
new vm.Script(`(${candidate})`, { filename: serverPath });
return candidate;
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
}
}
assert.fail(`无法完整提取服务端函数:${name}`);
}
const sourceFunctions = functionNames.map(extractFunction).join('\n');
const copy = (value) => JSON.parse(JSON.stringify(value));
const noop = () => {};
const sessionId = 'steer-recovery-review';
const answerId = 'form-answer-review';
const answerText = '表单答案:保留这条消息';
const oldOutput = '上一轮完整输出,不能被新轮覆盖。';
const noActiveErrors = [
'no active turn to steer',
'expected active turn id `old-turn-review`: no active turn',
];
function deferred() {
let resolve;
let reject;
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
return { promise, resolve, reject };
}
function createHarness({ running = true, failedSaveAttempts = [], steerError = null } = {}) {
const ws = {};
const timers = new Map();
const events = [];
const requests = [];
const starts = [];
const saves = [];
const stateWrites = [];
const failedSaves = new Set(failedSaveAttempts);
const requestResult = deferred();
const oldEntry = {
ws,
threadId: 'thread-review',
turnId: 'old-turn-review',
fullText: oldOutput,
toolCalls: [],
mcpContext: { sourceSessionId: sessionId },
startedAt: '2026-10-10T00:00:00.000Z',
};
let storedSession = {
id: sessionId,
agent: 'codexapp',
title: '恢复插入单测',
cwd: '/tmp/ccweb-steer-recovery-unit',
permissionMode: 'yolo',
codexAppThreadId: 'thread-review',
messages: [{
id: 'original-question',
role: 'user',
content: '上一轮问题',
timestamp: '2026-10-10T00:00:00.000Z',
}],
};
let retainedState = copy(oldEntry);
const writeState = (entry) => {
retainedState = copy(entry);
stateWrites.push(copy(entry));
return true;
};
const context = {
crypto, Date, Promise, Map,
activeCodexAppTurns: new Map([[sessionId, oldEntry]]),
activeCodexAppGoalCommands: new Map(),
activeCodexAppCompactions: new Map(),
codexAppGoalStates: new Map(),
activeProcesses: new Map(),
pendingCodexCapacityRetries: new Map(),
retainedCodexAppTurns: new Map(),
retiredCodexAppRuntimeTurns: new Map(),
giteaWorkflowWaiters: new Map(),
wsSessionMap: new Map(),
// 手动推进轮询定时器,不等待真实时间,也不向磁盘写会话。
setTimeout(callback, delay) {
const token = { unref: noop };
timers.set(token, { callback, delay });
return token;
},
clearTimeout: (token) => timers.delete(token),
loadSession: (id) => id === sessionId ? copy(storedSession) : null,
saveSession(session) {
saves.push(copy(session));
if (failedSaves.has(saves.length)) return false;
storedSession = copy(session);
return true;
},
writeCodexAppTurnState: (id, entry) => writeState(entry),
persistCodexAppTurnState: (id, entry) => writeState(entry),
cleanupCodexAppTurnState(id, entry) {
entry.codexAppStateCleaned = true;
retainedState = null;
},
codexAppClient: {
isRunning: () => running,
request(method, params, timeout) {
requests.push({ method, params: copy(params), timeout });
return steerError ? Promise.reject(new Error(steerError)) : requestResult.promise;
},
},
startCodexAppTurn(id, input) {
starts.push({ sessionId: id, input: copy(input) });
return Promise.resolve();
},
sanitizeId: (id) => id,
isCodexAppSession: (session) => session?.agent === 'codexapp',
getRuntimeSessionId: (session) => session.codexAppThreadId,
getDefaultSessionCwd: () => '/tmp/ccweb-steer-recovery-unit',
getSessionAgent: (session) => session.agent,
normalizeSession: noop,
normalizeGiteaSource: () => null,
resolveMessageAttachments: (attachments) => copy(attachments),
resolveComposerDecorators: (text) => ({ runtimeText: text, mentions: [] }),
codexAppInputFromMessage: (text, attachments) => [
...(text.trim() ? [{ type: 'text', text }] : []),
...attachments.map((attachment) => ({ type: 'image', id: attachment.id })),
],
attachClientRequestId: (payload) => payload,
detachWsFromActiveRuntimes: noop,
registerCrossConversationReplyRequest: noop,
unregisterCrossConversationReplyRequest: noop,
deletePendingCrossConversationReply: noop,
resolvePendingCodexAppUserInputsForSession: noop,
resolvePendingCodexAppApprovalsForSession: noop,
resolvePendingCodexAppElicitationsForSession: noop,
formatRuntimeError: (agent, text) => String(text),
codexAppTurnKey: (id, entry) => `${entry.threadId}:${entry.turnId}`,
shouldRetryCodexTransientFailure: () => false,
cancelCodexCapacityRetry: noop,
hasCodexAppTurnMessage: (session, key) => session.messages.some((message) => message.codexAppTurnKey === key),
ensureStableMessageId: (message) => ({ ...message, id: crypto.randomUUID() }),
isCodexAppGoalActive: () => false,
dispatchTaskBoardLifecycle: noop,
TASK_BOARD_LIFECYCLE_EVENTS: {
USER_MESSAGE_RECEIVED: 'user_message_received',
TURN_FAILED: 'turn_failed',
TURN_COMPLETED: 'turn_completed',
},
TASK_STATUS_CLASSIFICATION_EVENTS: {
USER_MESSAGE_RECEIVED: 'user_message_received',
TURN_COMPLETED: 'turn_completed',
},
enqueueTaskStatusClassification: () => Promise.resolve(),
crossConversationReplyRequestIds: () => [],
reconcilePendingCrossConversationReplies: noop,
flushPendingCrossConversationReplies: noop,
sanitizeMessageForTransport: copy,
resolveSessionHistory: (session) => session,
plog: noop,
sendSessionList: noop,
previewInlineText: (text) => text,
wsSend: (target, payload) => events.push(copy(payload)),
};
vm.createContext(context);
vm.runInContext(sourceFunctions, context, { filename: serverPath, timeout: 1000 });
return {
context, oldEntry, events, requests, starts, saves, stateWrites, requestResult,
session: () => copy(storedSession),
state: () => retainedState && copy(retainedState),
send(attachments = []) {
return context.handleCodexAppSteerMessage(ws, {
sessionId, clientMessageId: answerId, text: answerText, attachments,
}, { runtimeText: answerText });
},
complete: () => context.handleCodexAppTurnComplete(sessionId),
setRunning: (value) => { running = value; },
async poll() {
const waiting = [...timers.entries()].filter(([, timer]) => timer.delay === 50);
assert.equal(waiting.length, 1, '等待 ready 时应只有一个待执行的轮询');
const [token, timer] = waiting[0];
timers.delete(token);
timer.callback();
await new Promise(setImmediate);
},
drain: () => new Promise(setImmediate),
};
}
function answers(harness) {
return harness.session().messages.filter((message) => message.id === answerId);
}
function assertSaveFailure(harness) {
assert.equal(harness.starts.length, 0, '保存失败不能启动新轮');
assert.equal(harness.stateWrites.filter((entry) => entry.fullText === '').length, 0, '保存失败不能写入新轮的空运行态');
assert.ok(harness.events.some((event) => event.type === 'error' && event.code === 'session_save_failed'), '保存失败应返回明确的可重试错误');
assert.equal(harness.events.filter((event) => event.type === 'codex_app_steer_status').at(-1)?.status, 'failed', '保存失败不能把答案确认成发送成功');
}
async function testWaitingTurnEnded() {
const harness = createHarness({ running: false });
const attachment = { id: 'answer-image', filename: '答案图片.png', mime: 'image/png', kind: 'image' };
assert.equal(harness.send([attachment]).ok, true, '等待中的插入请求应被接收');
assert.equal(answers(harness).length, 0, '等待 ready 时不能预先重复保存答案');
harness.complete();
harness.setRunning(true);
await harness.poll();
assert.equal(harness.requests.length, 0, '旧轮已结束时应走普通消息分发');
assert.equal(harness.starts.length, 1, '旧轮结束后应只启动一轮普通发送');
assert.equal(answers(harness).length, 1, '重分发后答案应只保存一次');
assert.deepEqual(answers(harness)[0].attachments, [attachment], '转新轮必须保留答案附件');
assert.deepEqual(harness.session().messages.map((message) => message.role), ['user', 'assistant', 'user'], '历史应先保存旧轮输出,再保存新答案');
assert.ok(harness.events.some((event) => event.type === 'codex_app_steer_status' && event.status === 'sent'), '转新轮后应确认同一条答案发送成功');
}
async function testWaitingTurnReplaced() {
const harness = createHarness({ running: false });
harness.send();
harness.complete();
const replacement = { ...harness.oldEntry, turnId: 'replacement-turn-review', fullText: '', historySaved: undefined };
harness.context.activeCodexAppTurns.set(sessionId, replacement);
harness.setRunning(true);
await harness.poll();
assert.equal(harness.requests.length, 1, '已有新 entry 时应向当前轮发送一次插入请求');
assert.equal(harness.requests[0].method, 'turn/steer', '已有活动轮应保留插入语义');
assert.equal(harness.requests[0].params.expectedTurnId, replacement.turnId, '重新分发必须使用新 entry 的 turn id');
assert.equal(harness.starts.length, 0, '已有新 entry 时不能额外创建新轮');
assert.equal(answers(harness).length, 1, '重新分发到新 entry 时答案不能重复保存');
harness.requestResult.resolve({});
await harness.drain();
assert.ok(harness.events.some((event) => event.type === 'codex_app_steer_status' && event.status === 'inserted'), '当前轮插入成功后应确认答案');
}
async function testEndedTurnSaveFailed() {
const harness = createHarness({ failedSaveAttempts: [2], steerError: noActiveErrors[1] });
harness.send();
await harness.drain();
assertSaveFailure(harness);
assert.equal(harness.oldEntry.historySaved, false, '旧轮保存失败应记录明确状态');
assert.equal(harness.state()?.fullText, oldOutput, '旧轮完整输出运行态必须保留');
assert.equal(harness.state()?.turnStatus, 'completed', '保留的运行态应标记旧轮已经结束');
assert.equal(answers(harness).length, 1, '旧轮输出保存失败时不能删除已保存的答案');
}
async function testCompletedFirstReorderSaveFailed() {
const harness = createHarness({ failedSaveAttempts: [3] });
harness.send();
await harness.drain();
assert.equal(harness.requests.length, 1, '应先发出等待响应的 steer 请求');
harness.complete();
assert.equal(harness.oldEntry.historySaved, true, '先到的 completion 应成功保存旧轮输出');
harness.requestResult.reject(new Error(noActiveErrors[0]));
await harness.drain();
assertSaveFailure(harness);
assert.equal(harness.saves.length, 3, '应真实尝试保存旧轮输出与答案的重排顺序');
assert.equal(answers(harness).length, 1, '历史重排保存失败时应保留一次答案');
assert.deepEqual(harness.session().messages.map((message) => message.role), ['user', 'user', 'assistant'], '保存失败时不能误认为重排已经提交');
}
async function testErrorClassification() {
const harness = createHarness();
for (const message of noActiveErrors) {
assert.equal(harness.context.isCodexAppNoActiveTurnError(new Error(message)), true, `应识别真实的无活动轮错误:${message}`);
}
const otherErrors = [
'expected active turn id `old-turn-review`: active turn id `different-turn`',
'network connection reset',
'network error while checking no active turn to steer',
];
for (const message of otherErrors) {
assert.equal(harness.context.isCodexAppNoActiveTurnError(new Error(message)), false, `不能把其他错误误判成结束:${message}`);
const failedHarness = createHarness({ steerError: message });
failedHarness.send();
await failedHarness.drain();
assert.equal(failedHarness.starts.length, 0, `真实错误不能自动开新轮:${message}`);
assert.equal(failedHarness.context.activeCodexAppTurns.get(sessionId), failedHarness.oldEntry, '真实错误不能提前收敛仍活动的旧轮');
assert.equal(answers(failedHarness).length, 0, '未被运行时接收的答案应回滚,避免重试时重复保存');
assert.ok(failedHarness.events.some((event) => event.type === 'error' && event.code === 'codexapp_steer_failed'), '真实错误应保留插入失败语义');
}
}
async function main() {
const tests = [
['等待 ready 时旧轮结束后普通发送', testWaitingTurnEnded],
['等待 ready 时已有新 entry 按当前轮插入', testWaitingTurnReplaced],
['无活动轮但旧轮输出保存失败时保留运行态', testEndedTurnSaveFailed],
['completion 先到而重排保存失败时保留答案', testCompletedFirstReorderSaveFailed],
['无活动轮错误与真实错误区分', testErrorClassification],
];
// 独立脚本异常挂起时也会在 60 秒以内退出。
const deadline = setTimeout(() => {
console.error('codexapp-steer-recovery-unit:单测超过 45 秒,已终止。');
process.exit(1);
}, 45000);
try {
for (const [name, test] of tests) {
await test();
console.log(`通过:${name}`);
}
console.log(`codexapp-steer-recovery-unit:${tests.length} 项全部通过。`);
} finally {
clearTimeout(deadline);
}
}
main().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});