chore: rebuild CentOS7 release package

This commit is contained in:
shiyue
2026-07-03 08:53:37 +08:00
parent d816ae28b9
commit faf6adceb7
7 changed files with 499 additions and 82 deletions

View File

@@ -614,6 +614,102 @@ function assertFrontendMcpReloadContract() {
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
}
function assertSessionSwitchResilienceContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const runtimeSource = fs.readFileSync(path.join(REPO_DIR, 'lib', 'agent-runtime.js'), 'utf8');
assert(frontendSource.includes('SESSION_LOAD_REQUEST_TIMEOUT_MS'), 'Frontend should define a hard timeout for session load requests');
assert(frontendSource.includes('sessionLoadRequestTimer'), 'Frontend should track the session load request timeout timer');
assert(frontendSource.includes('function clearPendingSessionSwitchRequest'), 'Frontend should be able to cancel stale pending session switch requests');
assert(frontendSource.includes('function scheduleSessionLoadRequestTimeout'), 'Frontend should schedule cancellation for stuck load_session requests');
assert(frontendSource.includes('pendingSessionResumeRequest'), 'Frontend should track lightweight running-session resume requests');
assert(frontendSource.includes('SESSION_RESUME_FALLBACK_MS'), 'Frontend should keep a compatibility fallback for servers without resume_session');
assert(frontendSource.includes('function requestSessionResume'), 'Frontend should request running-session resume without full history reload');
assert(frontendSource.includes("type: 'resume_session'"), 'Frontend should use resume_session for reconnecting running conversations');
assert(frontendSource.includes("case 'resume_session_result':"), 'Frontend should handle lightweight resume results');
assert(frontendSource.includes('recoverCurrent: true'), 'Frontend fallback load_session should preserve the current running view');
const visibilityStart = frontendSource.indexOf("document.addEventListener('visibilitychange'");
const visibilityEnd = visibilityStart >= 0 ? frontendSource.indexOf("if (!authToken)", visibilityStart) : -1;
const visibilitySource = visibilityStart >= 0 && visibilityEnd > visibilityStart
? frontendSource.slice(visibilityStart, visibilityEnd)
: '';
assert(visibilitySource.includes('requestSessionResume(currentSessionId'), 'Visibility restore should use lightweight resume for running conversations');
assert(visibilitySource.includes("send({ type: 'list_sessions' });"), 'Visibility restore should only refresh session list for idle conversations');
assert(!visibilitySource.includes("type: 'load_session'"), 'Visibility restore must not force load_session and rerender the current conversation');
assert(!visibilitySource.includes('beginSessionSwitch('), 'Visibility restore must not force a session switch and scroll to bottom');
assert(
/else if \(currentSessionId && \(isGenerating \|\| currentSessionRunning\)[\s\S]*?pendingSessionResumeRequest\s*=\s*\{/.test(frontendSource),
'Frontend should queue a lightweight resume request for running conversations when WS closes'
);
assert(
/const flushedSessionResume = flushedSessionSwitch \? false : flushPendingSessionResume\(\);[\s\S]*?requestSessionResume\(currentSessionId/.test(frontendSource),
'Frontend should resume the current running session after auth without forcing load_session'
);
assert(
/case 'background_done':[\s\S]*?if \(isNearBottom\(\)\)[\s\S]*?openSession\(msg\.sessionId,\s*\{ forceSync: true, blocking: false \}\)[\s\S]*?send\(\{ type: 'list_sessions' \}\)/.test(frontendSource),
'Frontend should not auto-rerender the current session on background_done while the user is reading history'
);
assert(
/function startGenerating\(sessionId = currentSessionId, options = \{\}\)[\s\S]*?const shouldFollow = options\.follow !== false[\s\S]*?if \(shouldFollow\)/.test(frontendSource),
'Frontend resume_generating should be able to create a streaming bubble without forcing scroll-to-bottom'
);
assert(
/const preserveStreaming = !!\(options\.preserveStreaming[\s\S]*?\(isGenerating \|\| currentSessionRunning \|\| hasStreamingElement\)\)/.test(frontendSource),
'Frontend should preserve the current running conversation DOM even when isGenerating was stale'
);
assert(
/case 'session_history_chunk':[\s\S]*?activeSessionLoad\?\.recoverCurrent[\s\S]*?break;/.test(frontendSource),
'Frontend should ignore history chunks from recovery fallback to avoid duplicating/redrawing bubbles'
);
assert(
/function flushPendingSessionSwitch\(\)[\s\S]*?return false[\s\S]*?return true/.test(frontendSource),
'Frontend flushPendingSessionSwitch should report whether it sent a load_session request'
);
assert(
/pendingSessionSwitchRequest\s*=\s*\{[\s\S]*?blocking:\s*false[\s\S]*?requestId:\s*activeSessionLoad\.requestId/.test(frontendSource),
'Frontend should retry load_session after WS close without re-blocking the UI'
);
assert(
frontendSource.includes('!messageRequestId && !currentSessionId && !activeLoad && !pendingNewSession'),
'Frontend should not let late requestId-bearing session_info switch an idle/welcome view'
);
assert(
frontendSource.includes('matchesActiveLoadError') && frontendSource.includes('errorRequestId === activeSessionLoad.requestId'),
'Frontend should clear active session load errors by requestId'
);
assert(serverSource.includes('SESSION_TRANSPORT_MESSAGE_CONTENT_MAX_CHARS'), 'Server should cap session message size for WebSocket transport');
assert(serverSource.includes("case 'resume_session':"), 'Server should accept lightweight resume_session requests');
assert(serverSource.includes('function handleResumeSession'), 'Server should implement lightweight running-session resume');
assert(serverSource.includes('function attachActiveRuntimeToWs'), 'Server should share runtime re-attach logic without sending session_info first');
assert(serverSource.includes('WS_HEARTBEAT_MAX_MISSES'), 'Server should tolerate missed WebSocket pongs before terminating');
assert(serverSource.includes('function markWsActivity'), 'Server should mark WebSocket activity on send/message/pong');
assert(
serverSource.includes('markWsActivity(ws);') && serverSource.includes('markWsActivity(ws);'),
'Server should call markWsActivity from WebSocket send and message paths'
);
assert(
/hasRecentActivity[\s\S]*?_ccWebMissedPongs[\s\S]*?ws_heartbeat_terminate/.test(serverSource),
'Server heartbeat should consider recent activity and log before terminating stale sockets'
);
assert(serverSource.includes('function sanitizeMessagesForTransport'), 'Server should sanitize session messages before WebSocket transport');
assert(
/function splitHistoryMessages\(messages[\s\S]*?const list = sanitizeMessagesForTransport\(messages\)/.test(serverSource),
'Server history split should operate on transport-sanitized messages'
);
assert(
/function wsSend\(ws, data\)[\s\S]*?try\s*\{[\s\S]*?JSON\.stringify\(data\)[\s\S]*?catch/.test(serverSource),
'Server wsSend should guard JSON serialization failures'
);
assert(
runtimeSource.includes('CC_WEB_RUNTIME_FULL_TEXT_MAX_CHARS') &&
runtimeSource.includes('function appendCappedText') &&
runtimeSource.includes('entry.fullText = appendCappedText'),
'Classic runtime should cap accumulated fullText in memory'
);
}
async function main() {
assertFrontendGenerationControlsContract();
assertFrontendComposerMcpContract();
@@ -621,6 +717,7 @@ async function main() {
assertFrontendMarkdownLinkContract();
assertMockCodexAppPromptUserNotTextTriggered();
assertFrontendMcpReloadContract();
assertSessionSwitchResilienceContract();
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
const configDir = path.join(tempRoot, 'config');
@@ -756,6 +853,13 @@ async function main() {
const { ws, messages, token } = await connectWs(port, password);
await nextMessage(messages, ws, (msg) => msg.type === 'session_list');
ws.send(JSON.stringify({ type: 'load_session', sessionId: 'missing-session', requestId: 'reg-missing-session' }));
const missingSessionLoad = await nextMessage(messages, ws, (msg) => (
msg.type === 'error' &&
msg.code === 'session_not_found' &&
msg.sessionId === 'missing-session'
));
assert(missingSessionLoad.requestId === 'reg-missing-session', 'Missing load_session error should echo requestId for frontend cleanup');
const pickerRoot = path.join(homeDir, 'picker-root');
mkdirp(path.join(pickerRoot, 'alpha'));
@@ -1977,7 +2081,11 @@ async function main() {
assert(codexAppImportItemAfter?.alreadyImported === true, 'Codex App import listing should mark codexAppThreadId as imported');
ws.send(JSON.stringify({ type: 'delete_session', sessionId: importedCodexApp.sessionId }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && !msg.sessions.some((s) => s.id === importedCodexApp.sessionId));
await nextMessage(messages, ws, (msg) => (
msg.type === 'session_list' &&
!msg.sessions.some((s) => s.id === importedCodexApp.sessionId) &&
!fs.existsSync(importedCodexAppPath)
));
assert(!fs.existsSync(importedCodexAppPath), 'Deleting Codex App imported session did not remove cc-web session JSON');
assert(fs.existsSync(codexAppImportFixture.rolloutPath), 'Deleting Codex App imported session should keep rollout history for recovery');