chore: rebuild release package

This commit is contained in:
shiyue
2026-07-27 17:06:32 +08:00
parent 85f2f2d420
commit efe7c02ec3
12 changed files with 548 additions and 25 deletions

View File

@@ -618,8 +618,8 @@ function assertFrontendSidebarCollapseContract() {
'Rich themes should provide isolated rail treatments on top of the shared semantic fallback'
);
assert(
indexSource.includes('style.css?v=20260720-plan-progress-final-state')
&& indexSource.includes('app.js?v=20260720-plan-progress-final-state'),
indexSource.includes('style.css?v=20260727-session-item-tooltip')
&& indexSource.includes('app.js?v=20260727-session-item-tooltip'),
'Sidebar interaction assets should share the reviewed cache-busting version'
);
}
@@ -942,8 +942,8 @@ function assertPlanListProgressContract() {
assert(extractorSource.includes('references/source-assets/wasteland-icon-sheet.webp'), 'Plan progress extractor should read the archived source sheet');
assert(!extractorSource.includes('sessions/_attachments'), 'Plan progress extractor should not depend on temporary session attachments');
assert(indexSource.includes('style.css?v=20260720-plan-progress-final-state'), 'Plan progress CSS should use the current cache-busted URL');
assert(indexSource.includes('app.js?v=20260720-plan-progress-final-state'), 'Plan progress frontend logic should use the current cache-busted URL');
assert(indexSource.includes('style.css?v=20260727-session-item-tooltip'), 'Plan progress CSS should use the current cache-busted URL');
assert(indexSource.includes('app.js?v=20260727-session-item-tooltip'), 'Plan progress frontend logic should use the current cache-busted URL');
}
function assertFrontendGildedThemeContract() {
@@ -1058,8 +1058,8 @@ function assertFrontendGildedThemeContract() {
assert(contrast('#655446', '#fff7ea') >= 4.5, 'Gilded muted text should remain readable on ivory panels');
assert(contrast('#fff7ea', '#7a3f20') >= 7, 'Gilded primary action text should reach AAA contrast on copper');
assert(themeStyle.includes('@media (prefers-reduced-motion: reduce)'), 'Gilded theme motion should respect reduced-motion preferences');
assert(indexSource.includes('style.css?v=20260720-plan-progress-final-state'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
assert(indexSource.includes('app.js?v=20260720-plan-progress-final-state'), 'Theme bundle app script should use the current cache-busted asset URL');
assert(indexSource.includes('style.css?v=20260727-session-item-tooltip'), 'Theme bundle stylesheet should use the current cache-busted asset URL');
assert(indexSource.includes('app.js?v=20260727-session-item-tooltip'), 'Theme bundle app script should use the current cache-busted asset URL');
}
function assertFrontendWastelandThemeContract() {
@@ -1312,8 +1312,8 @@ function assertFrontendWastelandThemeContract() {
assert(contrast('#c9bda6', backgroundColor) >= 4.5, `Wasteland muted text should reach AA contrast on ${backgroundColor}`);
});
assert(indexSource.includes('style.css?v=20260720-plan-progress-final-state'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
assert(indexSource.includes('app.js?v=20260720-plan-progress-final-state'), 'Wasteland registration should share the cache-busted theme bundle URL');
assert(indexSource.includes('style.css?v=20260727-session-item-tooltip'), 'Wasteland stylesheet should share the cache-busted theme bundle URL');
assert(indexSource.includes('app.js?v=20260727-session-item-tooltip'), 'Wasteland registration should share the cache-busted theme bundle URL');
}
function assertFrontendCcwebPromptContract() {
@@ -2247,6 +2247,86 @@ function assertSetTitleMcpContract() {
assert(styleSource.includes('.session-item.llm-created:not(.pinned)::before'), 'LLM-created marker should be hidden when the session is pinned');
}
function assertSessionItemTooltipContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const tooltipSource = extractFunctionSource(frontendSource, 'buildSessionItemTooltip');
const buildSessionItemTooltip = new Function(`${tooltipSource}\nreturn buildSessionItemTooltip;`)();
const fullTitle = '这个标题显示不下,需要在悬停时完整展示';
assert(
buildSessionItemTooltip('cc-web', fullTitle) === `项目cc-web\n标题:${fullTitle}`,
'Session item tooltip should show the project and the complete conversation title on separate lines'
);
assert(
buildSessionItemTooltip('', fullTitle) === `标题:${fullTitle}`,
'Session item tooltip should still expose the complete title when no project is available'
);
const createItemSource = extractFunctionSource(frontendSource, 'createSessionListItem');
assert(createItemSource.includes('getSessionProjectName(session)'), 'Session item tooltip should reuse the canonical project-name resolver');
assert(createItemSource.includes('item.title = buildSessionItemTooltip('), 'Session card should apply the combined project/title tooltip');
assert(!createItemSource.includes('item.title = sessionCwd'), 'Session card should not fall back to a project-only tooltip');
}
function assertTitleHistoryOutlineContract() {
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
const normalizeTitleHistorySource = extractFunctionSource(serverSource, 'normalizeTitleHistory');
const normalizeServerHistory = new Function(`${normalizeTitleHistorySource}\nreturn normalizeTitleHistory;`)();
const serverHistory = normalizeServerHistory([
{ title: '无效日期', changedAt: 'not-a-date', messageIndex: 1, source: 'llm' },
{ title: '无效索引', changedAt: '2026-07-27T00:00:00.000Z', messageIndex: -1, source: 'llm' },
...Array.from({ length: 102 }, (_, index) => ({
title: `主题 ${index}`,
changedAt: new Date(Date.UTC(2026, 6, 27, 0, index)).toISOString(),
messageIndex: index,
source: 'llm',
})),
]);
assert(serverHistory.length === 100, 'Server should retain only the latest 100 valid title history events');
assert(serverHistory[0].title === '主题 2' && serverHistory.at(-1).title === '主题 101', 'Server title history should discard invalid events and trim from the oldest side');
const normalizeOutlineHistorySource = extractFunctionSource(frontendSource, 'normalizeOutlineTitleHistory');
const formatOutlineDateSource = extractFunctionSource(frontendSource, 'formatUserOutlineDate');
const buildOutlineTimelineSource = extractFunctionSource(frontendSource, 'buildUserOutlineTimelineItems');
const outlineApi = new Function(`
${normalizeOutlineHistorySource}
${formatOutlineDateSource}
${buildOutlineTimelineSource}
return { normalizeOutlineTitleHistory, formatUserOutlineDate, buildUserOutlineTimelineItems };
`)();
const beforeMidnight = new Date(2026, 6, 27, 23, 59, 0).toISOString();
const titleChangedAt = new Date(2026, 6, 27, 23, 59, 30).toISOString();
const afterMidnight = new Date(2026, 6, 28, 0, 1, 0).toISOString();
assert(outlineApi.formatUserOutlineDate(beforeMidnight) === '2026-07-27', 'Outline dates should use the browser local calendar day before midnight');
assert(outlineApi.formatUserOutlineDate(afterMidnight) === '2026-07-28', 'Outline dates should roll over at browser-local midnight');
const timeline = outlineApi.buildUserOutlineTimelineItems([
{ type: 'message', id: 'user-1', targetMessageId: 'hapi-message-user-1', label: '第一步', timestamp: beforeMidnight, messageIndex: 0 },
{ type: 'message', id: 'user-2', targetMessageId: 'hapi-message-user-2', label: '第二步', timestamp: afterMidnight, messageIndex: 2 },
], [
{ title: 'SQL 排查主题', changedAt: titleChangedAt, messageIndex: 1, source: 'llm' },
]);
assert(
JSON.stringify(timeline.map((item) => item.type)) === JSON.stringify(['date', 'message', 'title', 'date', 'message']),
'Outline timeline should merge date, message and title nodes in conversation order'
);
assert(timeline[0].label === '2026-07-27' && timeline[3].label === '2026-07-28', 'Outline should deduplicate dates and show YYYY-MM-DD only');
assert(timeline[1].messageNumber === 1 && timeline[4].messageNumber === 2, 'Only selectable message nodes should consume outline numbering');
assert(timeline[2].label === 'SQL 排查主题' && !timeline[2].targetMessageId, 'Title history nodes should be read-only timeline metadata');
const updateOutlineSource = extractFunctionSource(frontendSource, 'updateUserOutlinePanel');
assert(updateOutlineSource.includes('user-outline-title-event') && updateOutlineSource.includes('user-outline-date'), 'Outline renderer should include dedicated title and date nodes');
assert(updateOutlineSource.includes("item.type === 'message'"), 'Outline renderer should reserve buttons for selectable message nodes');
assert(/closest\('\.user-outline-item'\)/.test(frontendSource), 'Outline click delegation should only target selectable message buttons');
assert(styleSource.includes('.user-outline-title-event') && styleSource.includes('.user-outline-date'), 'Outline title and date nodes should have dedicated styles');
assert(/\.user-outline-date::before[\s\S]*\.user-outline-date::after/.test(styleSource), 'Outline dates should use the requested two-sided divider treatment');
assert(serverSource.includes('titleHistory: refreshedSession.titleHistory'), 'session_info should expose persisted title history to the active conversation only');
assert(frontendSource.includes('titleHistory: normalizeOutlineTitleHistory(payload.titleHistory)'), 'Frontend snapshots should preserve normalized title history');
}
function assertSessionSwitchResilienceContract() {
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
@@ -3566,6 +3646,17 @@ async function main() {
console.log('Session switch race regression checks passed.');
return;
}
if (regressionTarget === 'title-history-outline') {
assertSetTitleMcpContract();
assertTitleHistoryOutlineContract();
console.log('Title history outline regression checks passed.');
return;
}
if (regressionTarget === 'session-item-tooltip') {
assertSessionItemTooltipContract();
console.log('Session item tooltip regression checks passed.');
return;
}
throw new Error(`Unknown regression target: ${regressionTarget}`);
}
@@ -3585,6 +3676,8 @@ async function main() {
assertCodexAppRuntimeSubAgentActivityContract();
assertFrontendPrimaryCodexAppUiContract();
assertSetTitleMcpContract();
assertSessionItemTooltipContract();
assertTitleHistoryOutlineContract();
assertSessionSwitchResilienceContract();
assertSessionSwitchRaceContract();
assertCodexAppChildToolFallbackContract();
@@ -3978,12 +4071,15 @@ async function main() {
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');
assert(mcpSetTitle.body.titleEvent?.title === 'Concise MCP Title', 'MCP set title should return the persisted title event');
assert(mcpSetTitle.body.titleEvent?.messageIndex === storedBeforeMcpTitle.messages.length, 'Title event should anchor after the messages persisted before the 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');
assert(mcpTitleRenamed.titleEvent?.title === 'Concise MCP Title', 'MCP set title should push the title event to current viewers');
const mcpTitleList = await nextMessage(messages, ws, (msg) => (
msg.type === 'session_list' &&
msg.sessions.some((session) => (
@@ -3997,6 +4093,27 @@ async function main() {
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');
assert(storedAfterMcpTitle.titleHistory?.length === 1, 'MCP set title should append exactly one title history event');
assert(storedAfterMcpTitle.titleHistory[0].source === 'llm' && storedAfterMcpTitle.titleHistory[0].title === 'Concise MCP Title', 'Persisted title history should identify the LLM title change');
assert(!Number.isNaN(Date.parse(storedAfterMcpTitle.titleHistory[0].changedAt)), 'Persisted title history should include a valid change timestamp');
const unchangedMcpTitle = await callInternalMcp(port, internalMcpToken, {
tool: 'ccweb_set_title',
sourceSessionId: codexSession.sessionId,
args: { title: 'Concise MCP Title' },
});
assert(unchangedMcpTitle.status === 200 && unchangedMcpTitle.body?.changed === false, 'Repeated MCP title should report unchanged');
assert(!unchangedMcpTitle.body.titleEvent, 'Repeated MCP title should not create a fake title event');
const storedAfterUnchangedMcpTitle = JSON.parse(fs.readFileSync(codexTitleSessionPath, 'utf8'));
assert(storedAfterUnchangedMcpTitle.titleHistory?.length === 1, 'Repeated MCP title should not append title history');
ws.send(JSON.stringify({ type: 'load_session', sessionId: codexSession.sessionId, requestId: 'reg-title-history-load' }));
const titleHistorySessionInfo = await nextMessage(messages, ws, (msg) => (
msg.type === 'session_info' &&
msg.sessionId === codexSession.sessionId &&
msg.requestId === 'reg-title-history-load'
));
assert(titleHistorySessionInfo.titleHistory?.length === 1, 'session_info should restore persisted title history');
const mcpEmptyTitle = await callInternalMcp(port, internalMcpToken, {
tool: 'ccweb_set_title',
@@ -4032,6 +4149,7 @@ async function main() {
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');
assert(storedAfterManualLock.titleHistory?.length === 1, 'Ignored MCP title should not append a fake title history event');
const mcpRelativeCreate = await callInternalMcp(port, internalMcpToken, {
tool: 'ccweb_create_conversation',