chore: rebuild CentOS7 release package
This commit is contained in:
@@ -11,6 +11,7 @@ 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 PUBLIC_STYLE_PATH = path.join(REPO_DIR, 'public', 'style.css');
|
||||
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');
|
||||
@@ -563,6 +564,34 @@ function assertFrontendComposerMcpContract() {
|
||||
assert(source.includes("className = 'msg-mentions'"), 'Frontend should render a dedicated mention strip container');
|
||||
}
|
||||
|
||||
function assertFrontendSlashDraftPreservationContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
assert(source.includes('const pendingSlashDraftsByRequestId = new Map();'), 'Frontend should keep request-scoped slash drafts');
|
||||
assert(source.includes('function rememberPendingSlashDraft(requestId, text, sessionId, agent)'), 'Frontend should define slash draft capture');
|
||||
assert(source.includes('function applyPendingSlashDraftResponse(msg)'), 'Frontend should define slash draft response handling');
|
||||
assert(source.includes('if (!msg.preserveComposerDraft) return false;'), 'Frontend should restore slash drafts only for explicit failure responses');
|
||||
assert(source.includes("if (msgInput.value !== '' || pendingAttachments.length > 0) return false;"), 'Frontend should not overwrite user input typed after a slash request');
|
||||
assert(source.includes('rememberPendingSlashDraft(requestId, text, currentSessionId, currentAgent);'), 'Frontend should remember slash draft before clearing the composer');
|
||||
assert(source.includes("send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, requestId });"), 'Frontend slash sends should carry requestId');
|
||||
assert((source.match(/applyPendingSlashDraftResponse\(msg\);/g) || []).length >= 2, 'Frontend should handle slash draft restoration on system_message and error');
|
||||
|
||||
const sendStart = source.indexOf('function sendMessage()');
|
||||
const slashStart = source.indexOf("if (text.startsWith('/'))", sendStart);
|
||||
const rememberStart = source.indexOf('rememberPendingSlashDraft(requestId, text, currentSessionId, currentAgent);', slashStart);
|
||||
const modelPickerStart = source.indexOf("if (text === '/model' || text === '/model ')", slashStart);
|
||||
const modePickerStart = source.indexOf("if (text === '/mode' || text === '/mode ')", slashStart);
|
||||
assert(slashStart >= 0 && rememberStart > slashStart, 'Frontend should keep slash send branch explicit');
|
||||
assert(modelPickerStart > slashStart && modelPickerStart < rememberStart, 'Frontend /model picker should stay local and clear normally');
|
||||
assert(modePickerStart > slashStart && modePickerStart < rememberStart, 'Frontend /mode picker should stay local and clear normally');
|
||||
|
||||
assert(serverSource.includes('handleSlashCommand(ws, msg.text.trim(), msg.sessionId, msg.agent, msg);'), 'Server should pass client request metadata into slash handler');
|
||||
assert(serverSource.includes('function handleSlashCommand(ws, text, sessionId, fallbackAgent, source = {})'), 'Server slash handler should accept request metadata');
|
||||
assert(serverSource.includes('wsSend(ws, attachClientRequestId(base, source));'), 'Server slash responses should echo requestId');
|
||||
assert(serverSource.includes('preserveComposerDraft: true'), 'Server should explicitly mark slash failures as draft-preserving');
|
||||
assert(serverSource.includes('wsSend(ws, attachClientRequestId({') && serverSource.includes('...(msg.preserveComposerDraft ? { preserveComposerDraft: true } : {})'), 'Server runtime errors should echo requestId and draft-preservation metadata');
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -589,6 +618,10 @@ function assertFrontendCcwebPromptContract() {
|
||||
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');
|
||||
assert(source.includes('function clearCcwebPromptSelection(questionEl, question)'), 'ccweb prompt options should expose an explicit no-selection action');
|
||||
assert(source.includes("clearChoice.textContent = '不选择';"), 'ccweb prompt should render a visible no-selection control');
|
||||
assert(source.includes("const shouldDeselect = buttons.some"), 'Single-select ccweb prompt options should allow clicking the selected option to deselect');
|
||||
assert(source.includes("textarea.dataset.ccwebPromptSelectionText"), 'ccweb prompt should clear only option-generated answer text when selections are removed');
|
||||
}
|
||||
|
||||
function assertFrontendMarkdownLinkContract() {
|
||||
@@ -614,6 +647,21 @@ function assertFrontendMcpReloadContract() {
|
||||
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
|
||||
}
|
||||
|
||||
function assertSetTitleMcpContract() {
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
|
||||
|
||||
assert(serverSource.includes("case 'ccweb_set_title':"), 'Server should route ccweb_set_title through internal MCP');
|
||||
assert(serverSource.includes('function setCurrentConversationTitle'), 'Server should implement current-conversation title setting');
|
||||
assert(serverSource.includes('ignored because title is manually locked'), 'ccweb_set_title should report manual title locks as ignored success');
|
||||
assert(serverSource.includes("titleSource = 'manual'"), 'Manual UI rename should mark titleSource as manual');
|
||||
assert(serverSource.includes('CCWEB_TITLE_TOOL_INSTRUCTIONS'), 'Server should define model guidance for the title tool');
|
||||
assert(frontendSource.includes("createdFromKind || ''") && frontendSource.includes("' llm-created'"), 'Frontend should mark MCP-created sessions with llm-created class');
|
||||
assert(frontendSource.includes('snapshot.titleSource') && frontendSource.includes('snapshot.createdFromKind'), 'Frontend should preserve title metadata in session snapshots');
|
||||
assert(styleSource.includes('.session-item.llm-created:not(.pinned)::before'), 'LLM-created marker should be hidden when the session is pinned');
|
||||
}
|
||||
|
||||
function assertSessionSwitchResilienceContract() {
|
||||
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
@@ -713,10 +761,12 @@ function assertSessionSwitchResilienceContract() {
|
||||
async function main() {
|
||||
assertFrontendGenerationControlsContract();
|
||||
assertFrontendComposerMcpContract();
|
||||
assertFrontendSlashDraftPreservationContract();
|
||||
assertFrontendCcwebPromptContract();
|
||||
assertFrontendMarkdownLinkContract();
|
||||
assertMockCodexAppPromptUserNotTextTriggered();
|
||||
assertFrontendMcpReloadContract();
|
||||
assertSetTitleMcpContract();
|
||||
assertSessionSwitchResilienceContract();
|
||||
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
|
||||
@@ -969,6 +1019,7 @@ async function main() {
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-slash-mcp', trigger: '/', query: 'ccweb', sessionId: codexSession.sessionId, agent: 'codex' }));
|
||||
const slashMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-slash-mcp');
|
||||
assert(slashMcpComposer.items.some((item) => item.kind === 'mcp' && item.name === 'ccweb_list_conversations'), 'Composer slash suggestions should include ccweb MCP tools');
|
||||
assert(slashMcpComposer.items.some((item) => item.kind === 'mcp' && item.name === 'ccweb_set_title'), 'Composer slash suggestions should include ccweb_set_title MCP tool');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-slash-mcp-config', trigger: '/', query: 'reg', sessionId: codexSession.sessionId, agent: 'codex' }));
|
||||
const slashMcpConfigComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-slash-mcp-config');
|
||||
@@ -990,6 +1041,17 @@ async function main() {
|
||||
assert(!slashMcpRuntimeComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'regRuntime'), 'Composer slash suggestions should not infer MCP servers from session tool names');
|
||||
assert(!slashMcpRuntimeComposer.items.some((item) => item.kind === 'mcp' && item.itemType === 'server' && item.name === 'reg-state'), 'Composer slash suggestions should not infer MCP servers from mcp:server labels');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/grilling', sessionId: codexSession.sessionId, mode: 'plan', agent: 'codex', requestId: 'reg-unknown-slash-draft' }));
|
||||
const unknownSlashDraft = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.requestId === 'reg-unknown-slash-draft');
|
||||
assert(unknownSlashDraft.sessionId === codexSession.sessionId, 'Unknown slash draft response should stay scoped to the active session');
|
||||
assert(unknownSlashDraft.preserveComposerDraft === true, 'Unknown slash command should tell the frontend to restore the composer draft');
|
||||
assert(/未知指令: \/grilling/.test(unknownSlashDraft.message || ''), 'Unknown slash command should still show the normal failure hint');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'message', text: '/help', sessionId: codexSession.sessionId, mode: 'plan', agent: 'codex', requestId: 'reg-help-slash-draft' }));
|
||||
const helpSlashDraft = await nextMessage(messages, ws, (msg) => msg.type === 'system_message' && msg.requestId === 'reg-help-slash-draft');
|
||||
assert(!helpSlashDraft.preserveComposerDraft, 'Successful slash command responses should clear pending drafts without restoring input');
|
||||
assert(/可用指令/.test(helpSlashDraft.message || ''), 'Slash /help should keep the normal help output');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-skill', trigger: '$', query: 'reg', sessionId: codexSession.sessionId, agent: 'codex' }));
|
||||
const skillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-skill');
|
||||
assert(skillComposer.items.some((item) => item.kind === 'skill' && item.name === 'regression-skill'), 'Composer skill suggestions should include local Codex skill');
|
||||
@@ -1066,6 +1128,72 @@ async function main() {
|
||||
assert(mcpList.body.currentConversationId === codexSession.sessionId, 'MCP list should return current source conversation id');
|
||||
assert(mcpList.body.conversations.some((item) => item.id === codexSession.sessionId && !item.summary), 'MCP list should return lightweight session metadata without summary');
|
||||
|
||||
const codexTitleSessionPath = path.join(sessionsDir, `${codexSession.sessionId}.json`);
|
||||
const storedBeforeMcpTitle = JSON.parse(fs.readFileSync(codexTitleSessionPath, 'utf8'));
|
||||
const mcpSetTitle = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_set_title',
|
||||
sourceSessionId: codexSession.sessionId,
|
||||
args: { title: 'Concise MCP Title' },
|
||||
});
|
||||
assert(mcpSetTitle.status === 200 && mcpSetTitle.body?.ok, `MCP set title should succeed: ${JSON.stringify(mcpSetTitle.body)}`);
|
||||
assert(mcpSetTitle.body.changed === true && mcpSetTitle.body.ignored === false, 'MCP set title should report a real title change');
|
||||
assert(mcpSetTitle.body.previousTitle === storedBeforeMcpTitle.title, 'MCP set title should return previousTitle');
|
||||
assert(mcpSetTitle.body.lockedByUser === false, 'MCP set title should not report a user lock before manual rename');
|
||||
const mcpTitleRenamed = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_renamed' &&
|
||||
msg.sessionId === codexSession.sessionId &&
|
||||
msg.title === 'Concise MCP Title'
|
||||
));
|
||||
assert(mcpTitleRenamed.titleSource === 'llm', 'MCP set title should push llm titleSource to current viewers');
|
||||
const mcpTitleList = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_list' &&
|
||||
msg.sessions.some((session) => (
|
||||
session.id === codexSession.sessionId &&
|
||||
session.title === 'Concise MCP Title' &&
|
||||
session.titleSource === 'llm'
|
||||
))
|
||||
));
|
||||
assert(mcpTitleList.sessions.some((session) => session.id === codexSession.sessionId && session.updated === storedBeforeMcpTitle.updated), 'MCP set title should not bump session updated sorting timestamp');
|
||||
const storedAfterMcpTitle = JSON.parse(fs.readFileSync(codexTitleSessionPath, 'utf8'));
|
||||
assert(storedAfterMcpTitle.title === 'Concise MCP Title', 'MCP set title should persist title');
|
||||
assert(storedAfterMcpTitle.titleSource === 'llm', 'MCP set title should persist llm titleSource');
|
||||
assert(storedAfterMcpTitle.updated === storedBeforeMcpTitle.updated, 'MCP set title should not modify updated timestamp');
|
||||
|
||||
const mcpEmptyTitle = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_set_title',
|
||||
sourceSessionId: codexSession.sessionId,
|
||||
args: { title: ' ' },
|
||||
});
|
||||
assert(mcpEmptyTitle.status === 400 && mcpEmptyTitle.body?.code === 'invalid_title', 'MCP set title should reject empty titles');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'rename_session', sessionId: codexSession.sessionId, title: 'Manual Locked Title' }));
|
||||
const manualTitleRenamed = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_renamed' &&
|
||||
msg.sessionId === codexSession.sessionId &&
|
||||
msg.title === 'Manual Locked Title'
|
||||
));
|
||||
assert(manualTitleRenamed.titleSource === 'manual', 'Manual rename should push manual titleSource');
|
||||
await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_list' &&
|
||||
msg.sessions.some((session) => (
|
||||
session.id === codexSession.sessionId &&
|
||||
session.title === 'Manual Locked Title' &&
|
||||
session.titleSource === 'manual'
|
||||
))
|
||||
));
|
||||
const mcpLockedTitle = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_set_title',
|
||||
sourceSessionId: codexSession.sessionId,
|
||||
args: { title: 'Ignored LLM Title' },
|
||||
});
|
||||
assert(mcpLockedTitle.status === 200 && mcpLockedTitle.body?.ok, 'MCP set title should return ok when manually locked');
|
||||
assert(mcpLockedTitle.body.changed === false && mcpLockedTitle.body.ignored === true, 'MCP set title should report ignored under manual lock');
|
||||
assert(mcpLockedTitle.body.reason === 'ignored because title is manually locked', 'MCP set title should explain manual lock ignores');
|
||||
assert(mcpLockedTitle.body.title === 'Manual Locked Title' && mcpLockedTitle.body.lockedByUser === true, 'MCP set title should preserve manual title under lock');
|
||||
const storedAfterManualLock = JSON.parse(fs.readFileSync(codexTitleSessionPath, 'utf8'));
|
||||
assert(storedAfterManualLock.title === 'Manual Locked Title', 'Manual locked title should remain persisted after ignored MCP set title');
|
||||
assert(storedAfterManualLock.titleSource === 'manual', 'Manual locked titleSource should remain manual after ignored MCP set title');
|
||||
|
||||
const mcpRelativeCreate = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_create_conversation',
|
||||
sourceSessionId: codexSession.sessionId,
|
||||
@@ -1092,11 +1220,20 @@ async function main() {
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'background_done' && msg.sessionId === mcpCreate.body.conversationId);
|
||||
const storedMcpCreated = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${mcpCreate.body.conversationId}.json`), 'utf8'));
|
||||
assert(storedMcpCreated.title === 'MCP Created Conversation', 'MCP created conversation should persist the requested title');
|
||||
assert(storedMcpCreated.titleSource === 'system', 'MCP created conversation title should not be treated as user manual title');
|
||||
assert(storedMcpCreated.agent === 'codex', 'MCP created conversation should persist the inherited source agent');
|
||||
assert(storedMcpCreated.permissionMode === 'yolo', 'MCP created conversation should persist yolo as the default mode');
|
||||
assert(storedMcpCreated.createdFrom?.kind === 'mcp', 'MCP created conversation should persist mcp creation kind');
|
||||
assert(storedMcpCreated.createdFrom?.sourceSessionId === codexSession.sessionId, 'MCP created conversation should persist source metadata');
|
||||
assert(storedMcpCreated.messages.some((message) => message.content === 'mcp created initial prompt' && message.crossConversation?.sourceSessionId === codexSession.sessionId), 'MCP created conversation should persist the initial cross-conversation message');
|
||||
assert(storedMcpCreated.messages.some((message) => message.role === 'assistant' && /mcp created initial prompt/.test(String(message.content || ''))), 'MCP created conversation should run the initial prompt');
|
||||
await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_list' &&
|
||||
msg.sessions.some((session) => (
|
||||
session.id === mcpCreate.body.conversationId &&
|
||||
session.createdFromKind === 'mcp'
|
||||
))
|
||||
));
|
||||
|
||||
const mcpReplyCreateCwd = path.join(tempRoot, 'mcp-create-reply');
|
||||
mkdirp(mcpReplyCreateCwd);
|
||||
|
||||
Reference in New Issue
Block a user