chore: rebuild release package
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user