chore: rebuild CentOS7 release package
This commit is contained in:
@@ -10,6 +10,7 @@ const WebSocket = require('ws');
|
||||
const REPO_DIR = path.resolve(__dirname, '..');
|
||||
const SERVER_PATH = path.join(REPO_DIR, 'server.js');
|
||||
const PUBLIC_APP_PATH = path.join(REPO_DIR, 'public', 'app.js');
|
||||
const PUBLIC_INDEX_PATH = path.join(REPO_DIR, 'public', 'index.html');
|
||||
const MOCK_CLAUDE = path.join(REPO_DIR, 'scripts', 'mock-claude.js');
|
||||
const MOCK_CODEX = path.join(REPO_DIR, 'scripts', 'mock-codex.js');
|
||||
const MOCK_CODEX_APP_SERVER = path.join(REPO_DIR, 'scripts', 'mock-codex-app-server.js');
|
||||
@@ -503,6 +504,7 @@ function assertFrontendGenerationControlsContract() {
|
||||
|
||||
function assertFrontendComposerMcpContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
const requestStart = source.indexOf('function requestComposerSuggestions()');
|
||||
const requestEnd = source.indexOf('\n function handleComposerSuggestions', requestStart);
|
||||
assert(requestStart >= 0 && requestEnd > requestStart, 'Frontend should define requestComposerSuggestions before handleComposerSuggestions');
|
||||
@@ -520,13 +522,68 @@ function assertFrontendComposerMcpContract() {
|
||||
assert(menuStart >= 0 && menuEnd > menuStart, 'Frontend should define showCmdMenu before requestComposerSuggestions');
|
||||
const menuBlock = source.slice(menuStart, menuEnd);
|
||||
assert(/item\.kind\s*===\s*'mcp'/.test(menuBlock) && menuBlock.includes("'MCP'"), 'Composer menu should render MCP item labels');
|
||||
const selectStart = source.indexOf('function selectComposerItemByIndex(index)');
|
||||
const selectEnd = source.indexOf('\n function selectCmdMenuItem()', selectStart);
|
||||
assert(selectStart >= 0 && selectEnd > selectStart, 'Frontend should define selectComposerItemByIndex before selectCmdMenuItem');
|
||||
const selectBlock = source.slice(selectStart, selectEnd);
|
||||
assert(selectBlock.includes('const insertion = String(item.insertion || item.label || item.name || \'\');'), 'Composer should insert selected item text through the generic insertion path');
|
||||
assert(selectBlock.includes('const appendSpace = item.appendSpace !== false;'), 'Composer should honor appendSpace for generic MCP insertion');
|
||||
assert(!source.includes('function showCcwebPromptUserComposerModal'), 'Composer should not open a parameter builder for ccweb_prompt_user');
|
||||
assert(!source.includes('composer_mcp_tool_submit'), 'Frontend should not submit ccweb_prompt_user from slash composer as structured MCP args');
|
||||
assert(!serverSource.includes('composer_mcp_tool_submit'), 'Server should not accept slash-composer structured MCP tool submissions');
|
||||
assert(!source.includes('data-composer-mcp-questions'), 'Frontend should not render a slash-composer MCP argument builder');
|
||||
assert(!source.includes('data-option-field="recommended"'), 'Frontend should not render slash-composer MCP option editors');
|
||||
assert(source.includes('function renderComposerMentionsStrip(meta)'), 'Frontend should define composer mention strip renderer');
|
||||
assert(source.includes("className = 'msg-mentions'"), 'Frontend should render a dedicated mention strip container');
|
||||
}
|
||||
|
||||
function assertMockCodexAppPromptUserNotTextTriggered() {
|
||||
const source = fs.readFileSync(MOCK_CODEX_APP_SERVER, 'utf8');
|
||||
assert(!source.includes('codexapp runtime prompt mcp'), 'Mock Codex App should not expose a text-triggered ccweb_prompt_user path');
|
||||
assert(!source.includes('mcp-ccweb-prompt-user'), 'Regression should not depend on a mock ccweb_prompt_user tool call id');
|
||||
}
|
||||
|
||||
function assertFrontendCcwebPromptContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
|
||||
assert(source.includes('function createCcwebPromptElement(prompt, meta = {})'), 'Frontend should define ccweb prompt renderer');
|
||||
assert(source.includes("type: 'ccweb_prompt_user_response'"), 'Frontend should send ccweb prompt answers over WebSocket');
|
||||
assert(source.includes("type: 'ccweb_prompt_user_dismiss'"), 'Frontend should send ccweb prompt dismiss requests over WebSocket');
|
||||
assert(source.includes("case 'ccweb_prompt_user_update':"), 'Frontend should handle ccweb prompt status updates');
|
||||
assert(source.includes("case 'ccweb_prompt_user_remove':"), 'Frontend should remove submitted ccweb prompt bubbles');
|
||||
assert(source.includes('applyCcwebPromptUserUpdate(msg);'), 'Frontend should apply ccweb prompt updates to cached messages and DOM');
|
||||
assert(source.includes('removeCcwebPromptMessageFromSnapshot'), 'Frontend should remove submitted prompt messages from cached snapshots');
|
||||
assert(source.includes('renderPendingCcwebPrompts'), 'Frontend should render pending ccweb prompt reminders');
|
||||
assert(indexSource.includes('id="ccweb-prompt-outline-btn"') && indexSource.includes('class="ccweb-prompt-outline-anchor" hidden'), 'Frontend should expose a hidden ccweb prompt outline button');
|
||||
assert(source.includes('toggleCcwebPromptOutlinePanel') && source.includes('ccwebPromptOutlineBtn.dataset.count'), 'Frontend should render pending forms behind a compact outline button');
|
||||
assert(source.includes('dismissCcwebPrompt'), 'Frontend should allow users to ignore pending ccweb prompts');
|
||||
assert(source.includes('CCWEB_PROMPT_VIEW_MODE_STORAGE_KEY'), 'Frontend should persist ccweb prompt view mode');
|
||||
assert(source.includes("className = 'ccweb-prompt-tabs'"), 'Frontend should render ccweb prompt tabs for multi-question forms');
|
||||
assert(source.includes("className = 'pending-ccweb-prompt-dismiss'"), 'Frontend should render a compact dismiss action for pending prompts');
|
||||
assert(source.includes('card.dataset.viewMode = normalized'), 'Frontend should switch ccweb prompt card view mode');
|
||||
assert(source.includes('m.ccwebPrompt'), 'Message rebuild should render persisted ccweb prompt messages');
|
||||
assert(source.includes("className = 'ccweb-prompt-answer'"), 'Each ccweb prompt question should expose an editable answer textarea');
|
||||
}
|
||||
|
||||
function assertFrontendMcpReloadContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
assert(source.includes('function mcpStartupStatusToastText(status)'), 'Frontend should format MCP startup status toast text');
|
||||
assert(source.includes("payload.status && typeof payload.status === 'object'"), 'Frontend should preserve plain MCP status summary objects');
|
||||
assert(source.includes('data.mcpStatus'), 'Frontend reload button should consume reload-mcp mcpStatus payload');
|
||||
assert(source.includes("case 'mcp_startup_status':"), 'Frontend should handle pushed MCP startup status updates');
|
||||
assert(source.includes('showMcpStartupStatusToast'), 'Frontend should show explicit MCP startup status toasts');
|
||||
assert(source.includes('notifyReady: true'), 'Frontend should only show ready MCP startup toasts for explicit reload actions');
|
||||
assert(source.includes("state === 'ready' && !options.notifyReady"), 'Frontend should suppress background ready MCP startup toasts');
|
||||
assert(source.includes('MCP 已启动'), 'Frontend should expose a ready toast for ccweb MCP');
|
||||
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
assertFrontendGenerationControlsContract();
|
||||
assertFrontendComposerMcpContract();
|
||||
assertFrontendCcwebPromptContract();
|
||||
assertMockCodexAppPromptUserNotTextTriggered();
|
||||
assertFrontendMcpReloadContract();
|
||||
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
|
||||
const configDir = path.join(tempRoot, 'config');
|
||||
@@ -1125,6 +1182,7 @@ async function main() {
|
||||
.split('\n')
|
||||
.find((line) => line.includes(`"event":"process_spawn"`) && line.includes(crossTargetSession.sessionId.slice(0, 8)));
|
||||
assert(mcpSpawnLine && mcpSpawnLine.includes('mcp_servers.ccweb.command') && mcpSpawnLine.includes('mcp_servers.ccweb.env_vars'), 'Codex spawn should inject ccweb MCP config');
|
||||
assert(mcpSpawnLine.includes('server.js') && mcpSpawnLine.includes('--ccweb-mcp-server'), 'Codex spawn should launch ccweb MCP through server.js in Node mode');
|
||||
assert(!mcpSpawnLine.includes(internalMcpToken), 'Codex spawn log should not expose internal MCP token');
|
||||
const projectMcpSpawnLine = processLogAfterMcp
|
||||
.trim()
|
||||
@@ -1442,6 +1500,15 @@ async function main() {
|
||||
const reloadMcpResult = await postAuthedJson(port, token, `/api/sessions/${codexAppSession.sessionId}/reload-mcp`);
|
||||
assert(reloadMcpResult.sessionId === codexAppSession.sessionId, 'Codex App MCP reload should return the target session id');
|
||||
assert(reloadMcpResult.result?.reloaded === true, 'Codex App MCP reload should call app-server config/mcpServer/reload');
|
||||
assert(reloadMcpResult.mcpStatus?.server === 'ccweb', 'Codex App MCP reload should return ccweb server startup status');
|
||||
assert(reloadMcpResult.mcpStatus?.status === 'ready', 'Codex App MCP reload should surface ready startup status from app-server notification');
|
||||
assert(reloadMcpResult.mcpStatus?.hasStartupStatus === true, 'Codex App MCP reload should distinguish real startupStatus from pending fallback');
|
||||
const reloadMcpStatusText = JSON.stringify(reloadMcpResult.mcpStatus);
|
||||
assert(/CC_WEB_MCP_TOKEN=\[redacted\]/.test(reloadMcpStatusText), 'Codex App MCP reload status should redact token-looking values');
|
||||
assert(!reloadMcpStatusText.includes('mock-secret-token'), 'Codex App MCP reload status should not leak raw token-looking values');
|
||||
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
assert(storedCodexApp.codexAppMcpStartupStatus?.servers?.ccweb?.status === 'ready', 'Codex App MCP startup status should be persisted on the session');
|
||||
assert(!JSON.stringify(storedCodexApp.codexAppMcpStartupStatus).includes('mock-secret-token'), 'Persisted MCP startup status should not leak raw token-looking values');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp dynamic prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const codexAppDynamicTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'mcp-ccweb-list');
|
||||
@@ -1449,8 +1516,149 @@ async function main() {
|
||||
assert(/currentConversationId/.test(codexAppDynamicTool.result || ''), 'Codex App MCP tool should return ccweb conversation data');
|
||||
assert(/"hasCcwebMcpConfig": true/.test(codexAppDynamicTool.result || ''), 'Codex App thread/start should pass ccweb MCP config');
|
||||
assert(/"hasProjectMcpConfig": true/.test(codexAppDynamicTool.result || ''), 'Codex App thread/start should pass project MCP config from session cwd');
|
||||
assert(/server\.js/.test(codexAppDynamicTool.result || '') && /--ccweb-mcp-server/.test(codexAppDynamicTool.result || ''), 'Codex App ccweb MCP config should launch through server.js in Node mode');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-empty-slash-prompt-user-mcp', trigger: '/', query: '', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
||||
const codexAppEmptySlashMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-empty-slash-prompt-user-mcp');
|
||||
const emptySlashPromptUserIndex = codexAppEmptySlashMcpComposer.items.findIndex((item) => item.kind === 'mcp' && item.server === 'ccweb' && item.name === 'ccweb_prompt_user');
|
||||
const firstOtherCcwebMcpIndex = codexAppEmptySlashMcpComposer.items.findIndex((item) => item.kind === 'mcp' && item.server === 'ccweb' && item.name !== 'ccweb_prompt_user');
|
||||
assert(emptySlashPromptUserIndex >= 0, 'Codex App empty slash composer should include ccweb_prompt_user');
|
||||
assert(firstOtherCcwebMcpIndex < 0 || emptySlashPromptUserIndex < firstOtherCcwebMcpIndex, 'ccweb_prompt_user should be pinned before other ccweb MCP tools');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-prompt-user-mcp', trigger: '/', query: 'prompt_user', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
||||
const codexAppPromptUserMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-prompt-user-mcp');
|
||||
const promptUserComposerItem = codexAppPromptUserMcpComposer.items.find((item) => item.kind === 'mcp' && item.server === 'ccweb' && item.name === 'ccweb_prompt_user');
|
||||
assert(promptUserComposerItem, 'Codex App composer should show ccweb_prompt_user when ccweb MCP is runtime-configured');
|
||||
assert(promptUserComposerItem.itemType === 'tool', 'ccweb_prompt_user composer item should be a normal MCP tool suggestion');
|
||||
assert(!promptUserComposerItem.action, 'ccweb_prompt_user composer item should not declare a form action');
|
||||
assert(promptUserComposerItem.insertion === 'mcp:ccweb/ccweb_prompt_user', 'ccweb_prompt_user composer item should insert the MCP mention text');
|
||||
assert(promptUserComposerItem.appendSpace === true, 'ccweb_prompt_user composer item should append a space like other MCP suggestions');
|
||||
|
||||
const promptUserResult = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_prompt_user',
|
||||
sourceSessionId: codexAppSession.sessionId,
|
||||
sourceHopCount: 0,
|
||||
args: {
|
||||
title: '确认实现方案',
|
||||
description: '回归测试多问题表单',
|
||||
questions: [
|
||||
{
|
||||
id: 'ui_choice',
|
||||
title: '交互方式',
|
||||
question: '用哪种交互方式?',
|
||||
required: true,
|
||||
options: [
|
||||
{ id: 'fineui', label: 'FineUI 弹窗', recommended: true, answerText: '使用 FineUI 弹窗。' },
|
||||
{ id: 'prompt', label: '浏览器 prompt', answerText: '使用浏览器 prompt。' },
|
||||
],
|
||||
answerPlaceholder: '填写方案',
|
||||
},
|
||||
{
|
||||
id: 'button_id',
|
||||
title: '按钮 ID',
|
||||
question: '确认按钮 ID 是什么?',
|
||||
required: true,
|
||||
options: [
|
||||
{ id: 'confirm', label: 'btnShortageReleaseConfirm', recommended: true, answerText: '按钮 ID 固定为 btnShortageReleaseConfirm。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert(promptUserResult.status === 200 && promptUserResult.body?.ok, `MCP prompt user should render: ${JSON.stringify(promptUserResult.body)}`);
|
||||
assert(promptUserResult.body.status === 'rendered', 'MCP prompt user should return rendered status without waiting for user input');
|
||||
assert(promptUserResult.body.questionCount === 2, 'MCP prompt user should preserve multiple questions');
|
||||
const promptRendered = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_message' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.message?.ccwebPrompt?.id === promptUserResult.body.promptId
|
||||
));
|
||||
assert(promptRendered.message.ccwebPrompt.status === 'pending', 'Prompt message should start pending');
|
||||
assert(promptRendered.message.ccwebPrompt.questions?.length === 2, 'Prompt message should carry all questions to the UI');
|
||||
assert(promptRendered.message.ccwebPrompt.questions[0]?.options?.some((option) => option.id === 'fineui' && option.recommended === true), 'Prompt message should preserve recommended options');
|
||||
|
||||
const promptDismissResult = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_prompt_user',
|
||||
sourceSessionId: codexAppSession.sessionId,
|
||||
sourceHopCount: 0,
|
||||
args: {
|
||||
title: '可忽略表单',
|
||||
questions: [
|
||||
{
|
||||
id: 'dismiss_choice',
|
||||
title: '是否忽略',
|
||||
question: '这个表单会被忽略删除。',
|
||||
required: false,
|
||||
options: [
|
||||
{ id: 'skip', label: '忽略', answerText: '忽略这个表单。' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert(promptDismissResult.status === 200 && promptDismissResult.body?.ok, 'Dismissable MCP prompt should render');
|
||||
await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_message' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.message?.ccwebPrompt?.id === promptDismissResult.body.promptId
|
||||
));
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ccweb_prompt_user_dismiss',
|
||||
sessionId: codexAppSession.sessionId,
|
||||
promptId: promptDismissResult.body.promptId,
|
||||
}));
|
||||
const promptDismissed = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'ccweb_prompt_user_remove' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.promptId === promptDismissResult.body.promptId
|
||||
));
|
||||
assert(promptDismissed.reason === 'dismissed', 'Dismissed prompt remove event should carry dismissed reason');
|
||||
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
assert(!storedCodexApp.messages.some((message) => message.ccwebPrompt?.id === promptDismissResult.body.promptId), 'Dismissed prompt message should be removed from session history');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ccweb_prompt_user_response',
|
||||
sessionId: codexAppSession.sessionId,
|
||||
promptId: promptUserResult.body.promptId,
|
||||
answers: {
|
||||
ui_choice: {
|
||||
selectedOptionIds: ['fineui'],
|
||||
answerText: '使用 FineUI 弹窗,方便固定按钮 ID。',
|
||||
},
|
||||
button_id: {
|
||||
selectedOptionIds: ['confirm'],
|
||||
answerText: '按钮 ID 固定为 btnShortageReleaseConfirm。',
|
||||
},
|
||||
},
|
||||
}));
|
||||
const promptSubmitted = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'ccweb_prompt_user_remove' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.promptId === promptUserResult.body.promptId
|
||||
));
|
||||
assert(promptSubmitted.prompt?.status === 'submitted', 'Prompt remove event should carry submitted status');
|
||||
assert(promptSubmitted.prompt?.answers?.ui_choice?.selectedOptionLabels?.[0] === 'FineUI 弹窗', 'Prompt remove event should include selected option labels');
|
||||
const promptAnswerMessage = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_message' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
msg.message?.role === 'user' &&
|
||||
/我已回答 ccweb 提示的问题/.test(msg.message.content || '') &&
|
||||
/btnShortageReleaseConfirm/.test(msg.message.content || '')
|
||||
));
|
||||
assert(/使用 FineUI 弹窗,方便固定按钮 ID。/.test(promptAnswerMessage.message.content || ''), 'Prompt submission should become a normal user message with the free-form answer');
|
||||
const promptAnswerDelta = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'text_delta' &&
|
||||
msg.sessionId === codexAppSession.sessionId &&
|
||||
/btnShortageReleaseConfirm/.test(msg.text || '')
|
||||
));
|
||||
assert(/我已回答 ccweb 提示的问题/.test(promptAnswerDelta.text || ''), 'Prompt submission should trigger a Codex App turn with the answer text');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
const storedPromptMessage = storedCodexApp.messages.find((message) => message.ccwebPrompt?.id === promptUserResult.body.promptId);
|
||||
assert(!storedPromptMessage, 'Submitted prompt message should be removed from session history');
|
||||
assert(storedCodexApp.messages.some((message) => message.role === 'user' && /btnShortageReleaseConfirm/.test(String(message.content || ''))), 'Prompt response user message should persist in session history');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp subagent prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const ccwebMcpChildRunning = await nextMessage(messages, ws, (msg) =>
|
||||
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||
@@ -1542,7 +1750,7 @@ async function main() {
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'slow codexapp prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((s) => s.id === codexAppSession.sessionId && s.isRunning));
|
||||
await sleep(150);
|
||||
await sleep(500);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'message',
|
||||
text: 'runtime steer insert',
|
||||
|
||||
Reference in New Issue
Block a user